boolean_op.rs
1 use std::path::PathBuf; 2 3 use anyhow::anyhow; 4 use clap::{Args, Parser}; 5 use kurbo::BezPath; 6 use skrifa::{FontRef, MetadataProvider}; 7 use svg::Document; 8 9 use linesweeper::{ 10 generators, 11 topology::{BinaryWindingNumber, Topology}, 12 Point, 13 }; 14 use linesweeper_util::{outline_to_bezpath, svg_to_bezpaths}; 15 16 #[derive(Copy, Clone, Debug)] 17 enum Op { 18 Union, 19 Intersection, 20 Xor, 21 Difference, 22 ReverseDifference, 23 } 24 25 #[derive(Copy, Clone, Debug, clap::ValueEnum)] 26 enum Example { 27 Checkerboard, 28 SlantedCheckerboard, 29 Slanties, 30 Star, 31 } 32 33 #[derive(Parser)] 34 struct Cli { 35 #[arg(long)] 36 output: PathBuf, 37 38 #[command(flatten)] 39 input: Input, 40 41 #[arg(long)] 42 non_zero: bool, 43 44 #[arg(long)] 45 path_bool: bool, 46 47 #[arg(long)] 48 epsilon: Option<f64>, 49 } 50 51 #[derive(Args, Debug)] 52 #[group(required = true, multiple = false)] 53 struct Input { 54 input: Option<PathBuf>, 55 56 #[arg(long)] 57 example: Option<Example>, 58 59 #[arg(long)] 60 char: Option<Vec<char>>, 61 } 62 63 fn points_to_bez(ps: Vec<Point>) -> BezPath { 64 let mut ps = ps.into_iter(); 65 let mut ret = BezPath::new(); 66 if let Some(p) = ps.next() { 67 ret.move_to((p.x, p.y)); 68 } 69 for p in ps { 70 ret.line_to((p.x, p.y)); 71 } 72 ret.close_path(); 73 ret 74 } 75 76 fn contours_to_bezs((cs0, cs1): (Vec<Vec<Point>>, Vec<Vec<Point>>)) -> (BezPath, BezPath) { 77 ( 78 cs0.into_iter().flat_map(points_to_bez).collect(), 79 cs1.into_iter().flat_map(points_to_bez).collect(), 80 ) 81 } 82 83 fn get_contours(input: &Input) -> anyhow::Result<(BezPath, BezPath)> { 84 match (&input.input, &input.example, &input.char) { 85 (Some(path), None, None) => { 86 let input = std::fs::read_to_string(path)?; 87 let tree = usvg::Tree::from_str(&input, &usvg::Options::default())?; 88 let mut contours = svg_to_bezpaths(&tree).into_iter(); 89 let first = contours.next().unwrap(); 90 let rest = contours.flatten().collect(); 91 Ok((first, rest)) 92 } 93 (None, Some(example), None) => match example { 94 Example::Checkerboard => Ok(contours_to_bezs(generators::checkerboard(10))), 95 Example::SlantedCheckerboard => { 96 Ok(contours_to_bezs(generators::slanted_checkerboard(10))) 97 } 98 Example::Slanties => Ok(contours_to_bezs(generators::slanties(10))), 99 Example::Star => Ok(generators::star(20)), 100 }, 101 (None, None, Some(chars)) => { 102 let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 103 let font_path = format!("{ws}/tests/fonts/Inconsolata-Regular.ttf"); 104 let data = std::fs::read(&font_path).unwrap(); 105 let font_ref = FontRef::new(&data).unwrap(); 106 let charmap = font_ref.charmap(); 107 let paths = chars 108 .iter() 109 .map(|c| -> anyhow::Result<_> { 110 let id = charmap.map(*c).ok_or(anyhow!("{c} not in the charmap"))?; 111 let outline = font_ref 112 .outline_glyphs() 113 .get(id) 114 .ok_or(anyhow!("missing glyph for {c}"))?; 115 Ok(outline_to_bezpath(outline)) 116 }) 117 .collect::<Result<Vec<_>, _>>()?; 118 119 Ok((paths[0].clone(), paths[1..].iter().flatten().collect())) 120 } 121 _ => unreachable!(), 122 } 123 } 124 125 pub fn main() -> anyhow::Result<()> { 126 let args = Cli::parse(); 127 let (shape_a, shape_b) = get_contours(&args.input)?; 128 129 let eps = args.epsilon.unwrap_or(0.1); 130 let top = Topology::from_paths_binary(&shape_a, &shape_b, eps)?; 131 let bbox = top.bounding_box(); 132 let min_x = bbox.min_x(); 133 let min_y = bbox.min_y(); 134 let max_x = bbox.max_x(); 135 let max_y = bbox.max_y(); 136 let pad = 1.0 + eps; 137 let one_width = max_x - min_x + 2.0 * pad; 138 let one_height = max_y - min_y + 2.0 * pad; 139 let stroke_width = (max_y - min_y).max(max_x - max_y) / 512.0; 140 let mut document = svg::Document::new().set( 141 "viewBox", 142 (min_x - pad, min_y - pad, one_width * 3.0, one_height * 2.0), 143 ); 144 145 // Draw the original document. 146 for c in [&shape_a, &shape_b] { 147 let mut data = svg::node::element::path::Data::new(); 148 for el in c { 149 let p = |point: kurbo::Point| (point.x, point.y); 150 data = match el { 151 kurbo::PathEl::MoveTo(p0) => data.move_to(p(p0)), 152 kurbo::PathEl::LineTo(p0) => data.line_to(p(p0)), 153 kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to((p(p0), p(p1))), 154 kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to((p(p0), p(p1), p(p2))), 155 kurbo::PathEl::ClosePath => data.close(), 156 }; 157 } 158 159 let path = svg::node::element::Path::new() 160 .set("stroke", "black") 161 .set("stroke-width", stroke_width) 162 .set("stroke-linecap", "round") 163 .set("stroke-linejoin", "round") 164 .set("opacity", 0.2) 165 .set("fill", "none") 166 .set("d", data); 167 document = document.add(path); 168 } 169 170 let add_one = |doc, op, x_off, y_off| { 171 if args.path_bool { 172 add_path_bool_op(doc, op, &shape_a, &shape_b, x_off, y_off, stroke_width) 173 } else { 174 add_op(doc, op, args.non_zero, &top, x_off, y_off, stroke_width) 175 } 176 }; 177 178 document = add_one(document, Op::Union, one_width, 0.0); 179 document = add_one(document, Op::Intersection, one_width * 2.0, 0.0); 180 document = add_one(document, Op::Xor, 0.0, one_height); 181 document = add_one(document, Op::Difference, one_width, one_height); 182 document = add_one(document, Op::ReverseDifference, one_width * 2.0, one_height); 183 184 svg::save(&args.output, &document)?; 185 186 Ok(()) 187 } 188 189 #[allow(clippy::too_many_arguments)] 190 fn add_op( 191 mut doc: Document, 192 op: Op, 193 non_zero: bool, 194 top: &Topology<BinaryWindingNumber>, 195 x_off: f64, 196 y_off: f64, 197 stroke_width: f64, 198 ) -> Document { 199 let contours = top.contours(|w| { 200 let inside = |winding| { 201 if non_zero { 202 winding != 0 203 } else { 204 winding % 2 != 0 205 } 206 }; 207 208 match op { 209 Op::Union => inside(w.shape_a) || inside(w.shape_b), 210 Op::Intersection => inside(w.shape_a) && inside(w.shape_b), 211 Op::Xor => inside(w.shape_a) != inside(w.shape_b), 212 Op::Difference => inside(w.shape_a) && !inside(w.shape_b), 213 Op::ReverseDifference => inside(w.shape_b) && !inside(w.shape_a), 214 } 215 }); 216 217 let colors = [ 218 "#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012", 219 "#9B2226", 220 ]; 221 222 let mut color_idx = 0; 223 for group in contours.grouped() { 224 let mut data = svg::node::element::path::Data::new(); 225 226 for contour_idx in group { 227 let path = &contours[contour_idx].path; 228 for el in path.iter() { 229 data = match el { 230 kurbo::PathEl::MoveTo(p) => data.move_to((p.x + x_off, p.y + y_off)), 231 kurbo::PathEl::LineTo(p) => data.line_to((p.x + x_off, p.y + y_off)), 232 kurbo::PathEl::QuadTo(p0, p1) => data.quadratic_curve_to(( 233 (p0.x + x_off, p0.y + y_off), 234 (p1.x + x_off, p1.y + y_off), 235 )), 236 kurbo::PathEl::CurveTo(p0, p1, p2) => data.cubic_curve_to(( 237 (p0.x + x_off, p0.y + y_off), 238 (p1.x + x_off, p1.y + y_off), 239 (p2.x + x_off, p2.y + y_off), 240 )), 241 kurbo::PathEl::ClosePath => data.close(), 242 }; 243 } 244 data = data.close(); 245 } 246 let path = svg::node::element::Path::new() 247 .set("d", data) 248 .set("stroke", "black") 249 .set("stroke-width", stroke_width) 250 .set("stroke-linecap", "round") 251 .set("stroke-linejoin", "round") 252 .set("fill", colors[color_idx]); 253 doc = doc.add(path); 254 color_idx = (color_idx + 1) % colors.len(); 255 } 256 doc 257 } 258 259 fn bezpath_to_path_bool(p: &BezPath) -> Vec<path_bool::PathSegment> { 260 let mut ret = Vec::new(); 261 for seg in p.segments() { 262 let dv = |p: kurbo::Point| glam::DVec2::from((p.x, p.y)); 263 let seg = match seg { 264 kurbo::PathSeg::Line(line) => path_bool::PathSegment::Line(dv(line.p0), dv(line.p1)), 265 kurbo::PathSeg::Quad(q) => { 266 path_bool::PathSegment::Quadratic(dv(q.p0), dv(q.p1), dv(q.p2)) 267 } 268 kurbo::PathSeg::Cubic(c) => { 269 path_bool::PathSegment::Cubic(dv(c.p0), dv(c.p1), dv(c.p2), dv(c.p3)) 270 } 271 }; 272 ret.push(seg); 273 } 274 ret 275 } 276 277 #[allow(clippy::too_many_arguments)] 278 fn add_path_bool_op( 279 mut doc: Document, 280 op: Op, 281 shape_a: &BezPath, 282 shape_b: &BezPath, 283 x_off: f64, 284 y_off: f64, 285 stroke_width: f64, 286 ) -> Document { 287 let mut shape_a = bezpath_to_path_bool(shape_a); 288 let mut shape_b = bezpath_to_path_bool(shape_b); 289 let fill = path_bool::FillRule::EvenOdd; 290 let op = match op { 291 Op::Union => path_bool::PathBooleanOperation::Union, 292 Op::Intersection => path_bool::PathBooleanOperation::Intersection, 293 Op::Xor => path_bool::PathBooleanOperation::Exclusion, 294 Op::Difference => path_bool::PathBooleanOperation::Difference, 295 Op::ReverseDifference => { 296 std::mem::swap(&mut shape_a, &mut shape_b); 297 path_bool::PathBooleanOperation::Difference 298 } 299 }; 300 let result = path_bool::path_boolean(&shape_a, fill, &shape_b, fill, op).unwrap(); 301 302 let colors = [ 303 "#005F73", "#0A9396", "#94D2BD", "#E9D8A6", "#EE9B00", "#CA6702", "#BB3E03", "#AE2012", 304 "#9B2226", 305 ]; 306 307 let mut color_idx = 0; 308 for path in result { 309 let mut data = svg::node::element::path::Data::new(); 310 311 if path.is_empty() { 312 continue; 313 } 314 let start = path[0].start(); 315 data = data.move_to((start.x + x_off, start.y + y_off)); 316 for seg in path { 317 data = match seg { 318 path_bool::PathSegment::Line(_, p) => data.line_to((p.x + x_off, p.y + y_off)), 319 path_bool::PathSegment::Quadratic(_, p0, p1) => data.quadratic_curve_to(( 320 (p0.x + x_off, p0.y + y_off), 321 (p1.x + x_off, p1.y + y_off), 322 )), 323 path_bool::PathSegment::Cubic(_, p0, p1, p2) => data.cubic_curve_to(( 324 (p0.x + x_off, p0.y + y_off), 325 (p1.x + x_off, p1.y + y_off), 326 (p2.x + x_off, p2.y + y_off), 327 )), 328 path_bool::PathSegment::Arc(..) => unimplemented!(), 329 }; 330 } 331 data = data.close(); 332 let path = svg::node::element::Path::new() 333 .set("d", data) 334 .set("stroke", "black") 335 .set("stroke-width", stroke_width) 336 .set("stroke-linecap", "round") 337 .set("stroke-linejoin", "round") 338 .set("fill", colors[color_idx]); 339 doc = doc.add(path); 340 color_idx = (color_idx + 1) % colors.len(); 341 } 342 doc 343 }