/ examples / boolean_op.rs
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  }