From 48dba6d57fd82e5213c2e9f4b99a549341cf6ed5 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Sun, 18 Jan 2026 12:47:33 -0600 Subject: [PATCH] Add an example to help visualize ops written in svg format --- examples/visualize_op.rs | 131 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 examples/visualize_op.rs diff --git a/examples/visualize_op.rs b/examples/visualize_op.rs new file mode 100644 index 0000000..95eef07 --- /dev/null +++ b/examples/visualize_op.rs @@ -0,0 +1,131 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, bail}; +use clap::Parser; +use kurbo::BezPath; +use svg::Document; + +use linesweeper::{ + binary_op, + topology::{Contours, Topology}, + BinaryOp, FillRule, +}; + +#[derive(Parser)] +struct Cli { + #[arg(long)] + output: PathBuf, + + #[arg(long)] + input: PathBuf, +} + +pub fn main() -> anyhow::Result<()> { + let args = Cli::parse(); + let input = std::fs::read_to_string(&args.input)?; + let mut input_lines = input.lines(); + let op = input_lines.next().ok_or(anyhow!("no op line"))?; + let op = match op { + "union" => BinaryOp::Union, + "intersection" => BinaryOp::Intersection, + "difference" => BinaryOp::Difference, + "xor" => BinaryOp::Xor, + _ => bail!("unknown op {op}"), + }; + + let shape_a = input_lines.next().ok_or(anyhow!("no first shape"))?; + let shape_b = input_lines.next().ok_or(anyhow!("no second shape"))?; + + for extra_line in input_lines { + if !extra_line.trim().is_empty() { + eprintln!("ignoring extra line: {extra_line}"); + } + } + + let shape_a = BezPath::from_svg(shape_a)?; + let shape_b = BezPath::from_svg(shape_b)?; + + let eps = 1e-5; + let contours = binary_op(&shape_a, &shape_b, FillRule::EvenOdd, op)?; + let top = Topology::from_paths_binary(&shape_a, &shape_b, eps)?; + let bbox = top.bounding_box(); + let min_x = bbox.min_x(); + let min_y = bbox.min_y(); + let max_x = bbox.max_x(); + let max_y = bbox.max_y(); + let pad = 1.0 + eps; + 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 document = svg::Document::new().set( + "viewBox", + (min_x - pad, min_y - pad, one_width * 2.0, one_height), + ); + + // Draw the original document. + for (c, color) in [(&shape_a, "green"), (&shape_b, "blue")] { + let mut data = svg::node::element::path::Data::new(); + for el in c { + let p = |point: kurbo::Point| (point.x, point.y); + data = match el { + kurbo::PathEl::MoveTo(p0) => data.move_to(p(p0)), + kurbo::PathEl::LineTo(p0) => data.line_to(p(p0)), + kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((p(p0), p(p1))), + kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((p(p0), p(p1), p(p2))), + kurbo::PathEl::ClosePath => data.close(), + }; + } + + let path = svg::node::element::Path::new() + .set("stroke", "black") + .set("stroke-width", stroke_width) + .set("stroke-linecap", "round") + .set("stroke-linejoin", "round") + .set("opacity", 0.5) + .set("fill", color) + .set("d", data); + document = document.add(path); + } + + document = add_op(document, &contours, one_width, stroke_width); + + svg::save(&args.output, &document)?; + + Ok(()) +} + +fn add_op(mut doc: Document, contours: &Contours, x_off: f64, stroke_width: f64) -> Document { + for group in contours.grouped() { + let mut data = svg::node::element::path::Data::new(); + + for contour_idx in group { + let path = &contours[contour_idx].path; + for el in path.iter() { + data = match el { + kurbo::PathEl::MoveTo(p) => data.move_to((p.x + x_off, p.y)), + kurbo::PathEl::LineTo(p) => data.line_to((p.x + x_off, p.y)), + kurbo::PathEl::QuadTo(p0, p1) => { + data.quadratic_curve_to(((p0.x + x_off, p0.y), (p1.x + x_off, p1.y))) + } + kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to(( + (p0.x + x_off, p0.y), + (p1.x + x_off, p1.y), + (p2.x + x_off, p2.y), + )), + kurbo::PathEl::ClosePath => data.close(), + }; + } + 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", "pink"); + doc = doc.add(path); + } + doc +}