gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Laying LEDs along a path from an SVG drawing: its longest path becomes
//! the shape the Path tool carries, ready for a count.

use crate::fixture_definition_editor::shape::Shape;

/// The longest path of an SVG drawing as a finished shape, at the size the
/// drawing gives it and centred on the fixture's origin, and how many paths
/// the drawing has.
pub(crate) fn path_from_svg(svg: &str) -> Result<(Shape, usize), String> {
    let outlines = gled_svg::ParsedSvg::parse(svg)
        .map_err(|error| error.to_string())?
        .outlines();
    let longest = outlines
        .iter()
        .max_by(|a, b| a.length_mm().total_cmp(&b.length_mm()))
        .ok_or("the drawing has no path")?;
    let (mut low, mut high) = ([f32::MAX; 2], [f32::MIN; 2]);
    for point in &longest.points_mm {
        for axis in 0..2 {
            low[axis] = low[axis].min(point[axis]);
            high[axis] = high[axis].max(point[axis]);
        }
    }
    let centre = [(low[0] + high[0]) / 2.0, (low[1] + high[1]) / 2.0];
    let points = longest
        .points_mm
        .iter()
        .map(|point| {
            [
                (point[0] - centre[0]) / 1000.0,
                (point[1] - centre[1]) / 1000.0,
            ]
        })
        .collect();
    let shape = Shape::Path {
        points,
        closed: longest.closed,
        finished: true,
    };
    Ok((shape, outlines.len()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn an_svg_circle_becomes_a_closed_path_round_the_origin() {
        let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="200mm" height="200mm" viewBox="0 0 200 200"><path d="M 0 0 L 10 0" stroke="black"/><circle cx="120" cy="80" r="40" fill="none" stroke="black"/></svg>"#;
        let (shape, paths) = path_from_svg(svg).expect("an SVG with paths");
        assert_eq!(paths, 2);
        let Shape::Path {
            points,
            closed,
            finished,
        } = shape
        else {
            panic!("a path");
        };
        assert!(closed && finished);
        // the circle, 40 mm in radius, now round the origin
        let reach = points
            .iter()
            .fold(0.0f32, |reach, p| reach.max(p[0].hypot(p[1])));
        assert!((reach - 0.04).abs() < 0.001, "{reach}");
    }
}