linesweeper 0.0.3

Robust sweep-line algorithm and two-dimensional boolean ops
Documentation
use std::path::PathBuf;

use clap::Parser;

use linesweeper::{boolean_op, num::CheapOrderedFloat, Point};

type Float = CheapOrderedFloat;

#[derive(Copy, Clone, Debug, clap::ValueEnum)]
enum Example {
    Checkerboard,
    SlantedCheckerboard,
    Slanties,
}

#[derive(Parser)]
struct Cli {
    input: PathBuf,
    output: PathBuf,

    #[arg(long)]
    non_zero: bool,

    #[arg(long)]
    epsilon: Option<f64>,
}

pub fn main() -> anyhow::Result<()> {
    let args = Cli::parse();
    let base = std::fs::read_to_string(args.input.join("base.json"))?;
    let cutout = std::fs::read_to_string(args.input.join("cutout.json"))?;
    let modifier = std::fs::read_to_string(args.input.join("modifier.json"))?;

    let base_contours: Vec<Vec<Point<Float>>> = serde_json::from_str(&base)?;
    let cutout_contours: Vec<Vec<Point<Float>>> = serde_json::from_str(&cutout)?;
    let modifier_contours: Vec<Vec<Point<Float>>> = serde_json::from_str(&modifier)?;

    let convert_contours = |cs: &[Vec<Point<Float>>]| {
        cs.iter()
            .map(|ps| {
                ps.iter()
                    .map(|Point { x, y }| (x.into_inner(), y.into_inner()))
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>()
    };
    let base_contours = convert_contours(&base_contours);
    let cutout_contours = convert_contours(&cutout_contours);
    let modifier_contours = convert_contours(&modifier_contours);

    let sub = boolean_op(
        &base_contours,
        &cutout_contours,
        linesweeper::FillRule::EvenOdd,
        linesweeper::BooleanOp::Difference,
    )
    .unwrap();
    let sub_contours = sub.contours().map(|c| c.points.clone()).collect::<Vec<_>>();
    let sub = convert_contours(&sub_contours);
    let result = boolean_op(
        &sub,
        &modifier_contours,
        linesweeper::FillRule::EvenOdd,
        linesweeper::BooleanOp::Union,
    )
    .unwrap();

    let (xs, ys): (Vec<_>, Vec<_>) = result
        .contours()
        .flat_map(|c| &c.points)
        .map(|p| (p.x, p.y))
        .unzip();
    let min_x = xs.iter().min().unwrap().into_inner();
    let max_x = xs.iter().max().unwrap().into_inner();
    let min_y = ys.iter().min().unwrap().into_inner();
    let max_y = ys.iter().max().unwrap().into_inner();
    let pad = 1.0;
    let one_width = max_x - min_x + 2.0 * pad;
    let one_height = max_y - min_y + 2.0 * pad;
    let stroke_width = (max_y - min_y).max(max_x - max_y) / 512.0;
    let mut doc = svg::Document::new().set(
        "viewBox",
        (min_x - pad, min_y - pad, one_width * 3.0, one_height * 2.0),
    );

    let colors = [
        "#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012",
        "#9B2226",
    ];

    let mut color_idx = 0;
    for group in result.grouped() {
        let mut data = svg::node::element::path::Data::new();

        for contour_idx in group {
            let mut contour = result[contour_idx].points.iter().cloned();
            let Some(p) = contour.next() else {
                continue;
            };

            let (x, y) = (p.x.into_inner(), p.y.into_inner());
            data = data.move_to((x, y));
            for p in contour {
                let (x, y) = (p.x.into_inner(), p.y.into_inner());
                data = data.line_to((x, y));
            }
            data = data.close();
        }
        let path = svg::node::element::Path::new()
            .set("d", data)
            .set("stroke", "black")
            .set("stroke-width", stroke_width)
            .set("stroke-linecap", "round")
            .set("stroke-linejoin", "round")
            .set("fill", colors[color_idx]);
        doc = doc.add(path);
        color_idx = (color_idx + 1) % colors.len();
    }

    svg::save(&args.output, &doc)?;

    Ok(())
}