/ tests / snapshots.rs
snapshots.rs
  1  use kurbo::{Affine, BezPath, ParamCurve as _, Point, Rect, Vec2};
  2  use libtest_mimic::{Arguments, Failed, Trial};
  3  use linesweeper::{
  4      sweep::{SweepLineBuffers, SweepLineRange, SweepLineRangeBuffers, Sweeper},
  5      topology::Topology,
  6      Segment, Segments,
  7  };
  8  use std::{
  9      collections::HashMap,
 10      path::{Path, PathBuf},
 11  };
 12  use tiny_skia::Pixmap;
 13  
 14  fn main() {
 15      let args = Arguments::from_args();
 16      let mut tests = sweep_snapshot_diffs();
 17      tests.extend(position_snapshot_diffs());
 18  
 19      libtest_mimic::run(&args, tests).exit();
 20  }
 21  
 22  fn path_color(idx: usize) -> tiny_skia::Color {
 23      let palette = [
 24          tiny_skia::Color::from_rgba8(0x00, 0x5F, 0x73, 0xFF),
 25          tiny_skia::Color::from_rgba8(0x94, 0xD2, 0xBD, 0xFF),
 26          tiny_skia::Color::from_rgba8(0xE9, 0xD8, 0xA6, 0xFF),
 27          tiny_skia::Color::from_rgba8(0xEE, 0x9B, 0x00, 0xFF),
 28          tiny_skia::Color::from_rgba8(0xCA, 0x67, 0x02, 0xFF),
 29          tiny_skia::Color::from_rgba8(0xBB, 0x3E, 0x03, 0xFF),
 30          tiny_skia::Color::from_rgba8(0xAE, 0x20, 0x12, 0xFF),
 31      ];
 32      palette[idx % palette.len()]
 33  }
 34  
 35  fn sweep_line_color() -> tiny_skia::Color {
 36      tiny_skia::Color::from_rgba8(0x9B, 0x22, 0x26, 0xFF)
 37  }
 38  
 39  fn sweep_snapshot_diffs() -> Vec<Trial> {
 40      let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
 41      let paths = glob::glob(&format!("{ws}/tests/snapshots/inputs/sweep/**/*.svg")).unwrap();
 42      paths
 43          .into_iter()
 44          .map(|p| {
 45              let p = p.unwrap();
 46              let name = input_path_base(&p).display().to_string();
 47              Trial::test(name, || generate_sweep_snapshot(p))
 48          })
 49          .collect()
 50  }
 51  
 52  fn position_snapshot_diffs() -> Vec<Trial> {
 53      let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
 54      let paths = glob::glob(&format!("{ws}/tests/snapshots/inputs/position/**/*.svg")).unwrap();
 55  
 56      paths
 57          .into_iter()
 58          .map(|p| {
 59              let p = p.unwrap();
 60              let name = input_path_base(&p).display().to_string();
 61              Trial::test(name, || generate_position_snapshot(p))
 62          })
 63          .collect()
 64  }
 65  
 66  fn input_path_base(input_path: &Path) -> &Path {
 67      let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
 68      let base = format!("{ws}/tests/snapshots/inputs");
 69      input_path.strip_prefix(base).unwrap()
 70  }
 71  
 72  fn output_path_for(input_path: &Path) -> PathBuf {
 73      let mut ws: PathBuf = std::env::var_os("CARGO_MANIFEST_DIR").unwrap().into();
 74      ws.push("target/snapshots/snapshots");
 75      ws.push(input_path);
 76      ws.set_extension("png");
 77      ws
 78  }
 79  
 80  fn saved_snapshot_path_for(input_path: &Path) -> PathBuf {
 81      let mut ws: PathBuf = std::env::var_os("CARGO_MANIFEST_DIR").unwrap().into();
 82      ws.push("tests/snapshots/snapshots");
 83      ws.push(input_path);
 84      ws.set_extension("png");
 85      ws
 86  }
 87  
 88  fn skia_path(elts: impl IntoIterator<Item = kurbo::PathEl>) -> tiny_skia::Path {
 89      let mut pb = tiny_skia::PathBuilder::new();
 90      for elt in elts {
 91          match elt {
 92              kurbo::PathEl::MoveTo(p) => pb.move_to(p.x as f32, p.y as f32),
 93              kurbo::PathEl::LineTo(p) => pb.line_to(p.x as f32, p.y as f32),
 94              kurbo::PathEl::QuadTo(p0, p1) => {
 95                  pb.quad_to(p0.x as f32, p0.y as f32, p1.x as f32, p1.y as f32)
 96              }
 97              kurbo::PathEl::CurveTo(p0, p1, p2) => pb.cubic_to(
 98                  p0.x as f32,
 99                  p0.y as f32,
100                  p1.x as f32,
101                  p1.y as f32,
102                  p2.x as f32,
103                  p2.y as f32,
104              ),
105              kurbo::PathEl::ClosePath => pb.close(),
106          }
107      }
108      pb.finish().unwrap()
109  }
110  
111  fn skia_kurbo_seg(seg: kurbo::PathSeg) -> tiny_skia::Path {
112      let mut pb = tiny_skia::PathBuilder::new();
113      let start = seg.start();
114      pb.move_to(start.x as f32, start.y as f32);
115      match seg {
116          kurbo::PathSeg::Line(ell) => pb.line_to(ell.p1.x as f32, ell.p1.y as f32),
117          kurbo::PathSeg::Quad(q) => {
118              pb.quad_to(q.p1.x as f32, q.p1.y as f32, q.p2.x as f32, q.p2.y as f32)
119          }
120          kurbo::PathSeg::Cubic(c) => pb.cubic_to(
121              c.p1.x as f32,
122              c.p1.y as f32,
123              c.p2.x as f32,
124              c.p2.y as f32,
125              c.p3.x as f32,
126              c.p3.y as f32,
127          ),
128      }
129      pb.finish().unwrap()
130  }
131  
132  fn skia_cubic(s: &kurbo::CubicBez) -> tiny_skia::Path {
133      let mut pb = tiny_skia::PathBuilder::new();
134      pb.move_to(s.p0.x as f32, s.p0.y as f32);
135      pb.cubic_to(
136          s.p1.x as f32,
137          s.p1.y as f32,
138          s.p2.x as f32,
139          s.p2.y as f32,
140          s.p3.x as f32,
141          s.p3.y as f32,
142      );
143      pb.finish().unwrap()
144  }
145  
146  fn skia_segment(s: &Segment) -> tiny_skia::Path {
147      skia_cubic(&s.to_kurbo_cubic())
148  }
149  
150  fn line(p: impl Into<Point>, q: impl Into<Point>) -> tiny_skia::Path {
151      let mut pb = tiny_skia::PathBuilder::new();
152      let p = p.into();
153      pb.move_to(p.x as f32, p.y as f32);
154      let q = q.into();
155      pb.line_to(q.x as f32, q.y as f32);
156      pb.finish().unwrap()
157  }
158  
159  fn two_lines(p: impl Into<Point>, q: impl Into<Point>, r: impl Into<Point>) -> tiny_skia::Path {
160      let mut pb = tiny_skia::PathBuilder::new();
161      let p = p.into();
162      pb.move_to(p.x as f32, p.y as f32);
163      let q = q.into();
164      pb.line_to(q.x as f32, q.y as f32);
165      let r = r.into();
166      pb.line_to(r.x as f32, r.y as f32);
167      pb.finish().unwrap()
168  }
169  
170  fn draw_orig_path(pixmap: &mut Pixmap, path: &BezPath, offset: kurbo::Point) {
171      let p = skia_path(path);
172      let mut paint = tiny_skia::Paint::default();
173      paint.set_color_rgba8(0, 0, 0, 255);
174      let stroke = tiny_skia::Stroke {
175          width: 1.0,
176          ..Default::default()
177      };
178      let transform = tiny_skia::Transform::from_translate(offset.x as f32, offset.y as f32);
179  
180      pixmap.stroke_path(&p, &paint, &stroke, transform, None);
181  }
182  
183  fn adjust_x_positions(orig: &[f64], padding: f64, min_x: f64, max_x: f64) -> Vec<f64> {
184      if orig.is_empty() {
185          return Vec::new();
186      }
187  
188      let mut padded = Vec::with_capacity(orig.len());
189      let mut max_so_far = f64::NEG_INFINITY;
190      for &x in orig {
191          let x = x.max(max_so_far + padding);
192          padded.push(x);
193          max_so_far = x;
194      }
195  
196      let orig_first = *orig.first().unwrap();
197      let orig_last = *orig.last().unwrap();
198      let padded_min = *padded.first().unwrap();
199      let padded_max = *padded.last().unwrap();
200  
201      let mut mid_shift = (orig_first + orig_last - padded_min - padded_max) / 2.0;
202      mid_shift = mid_shift.clamp(min_x - padded_min, max_x - padded_max);
203      if padded_max - padded_min > max_x - min_x {
204          mid_shift = (max_x + min_x - padded_max - padded_min) / 2.0;
205      }
206      for x in &mut padded {
207          *x += mid_shift;
208      }
209      padded
210  }
211  
212  fn color(c: tiny_skia::Color) -> tiny_skia::Paint<'static> {
213      let mut p = tiny_skia::Paint::default();
214      p.set_color(c);
215      p
216  }
217  
218  fn draw_sweep_line_range(
219      pixmap: &mut Pixmap,
220      segments: &Segments,
221      range: SweepLineRange<'_, '_, '_>,
222      bbox: kurbo::Rect,
223      padding: f64,
224  ) {
225      let y = range.line().y();
226      let mut paint = tiny_skia::Paint::default();
227      paint.set_color(sweep_line_color());
228      let stroke = tiny_skia::Stroke {
229          width: 2.0,
230          ..Default::default()
231      };
232      let thick_stroke = tiny_skia::Stroke {
233          width: 4.0,
234          ..Default::default()
235      };
236      let s_line = line(
237          (bbox.min_x() - padding, bbox.min_y() + y),
238          (bbox.max_x() + padding, bbox.min_y() + y),
239      );
240  
241      pixmap.stroke_path(
242          &s_line,
243          &paint,
244          &stroke,
245          tiny_skia::Transform::identity(),
246          None,
247      );
248  
249      let old_segs: Vec<_> = range.old_segment_range().collect();
250      let new_segs: Vec<_> = range.segment_range().collect();
251      let really_new_segs = new_segs.iter().filter(|s| !old_segs.contains(s));
252      let seg_color: HashMap<_, _> = old_segs
253          .iter()
254          .chain(really_new_segs.clone())
255          .enumerate()
256          .map(|(color_idx, seg_idx)| (seg_idx, color_idx))
257          .collect();
258      let origin = bbox.origin();
259  
260      for seg_idx in old_segs.iter().chain(really_new_segs) {
261          let color_idx = seg_color[seg_idx];
262          let seg = &segments[*seg_idx];
263          let p = skia_segment(seg);
264          let mut paint = tiny_skia::Paint::default();
265          paint.set_color(path_color(color_idx));
266          let transform = tiny_skia::Transform::from_translate(origin.x as f32, origin.y as f32);
267          pixmap.stroke_path(&p, &paint, &stroke, transform, None);
268      }
269  
270      let old_seg_positions: Vec<_> = old_segs.iter().map(|s| segments[*s].at_y(y)).collect();
271      let padded_old_seg_positions: Vec<_> = adjust_x_positions(
272          &old_seg_positions,
273          padding / 2.0,
274          bbox.min_x() - padding,
275          bbox.max_x() + padding,
276      );
277  
278      for ((&px, &x), seg_idx) in padded_old_seg_positions
279          .iter()
280          .zip(&old_seg_positions)
281          .zip(&old_segs)
282      {
283          let p = two_lines(
284              (bbox.min_x() + px, bbox.min_y()),
285              (bbox.min_x() + px, bbox.min_y() + y - padding / 2.0),
286              (bbox.min_x() + x, bbox.min_y() + y),
287          );
288          let color_idx = seg_color[seg_idx];
289          let c = path_color(color_idx);
290  
291          pixmap.stroke_path(
292              &p,
293              &color(c),
294              &thick_stroke,
295              tiny_skia::Transform::identity(),
296              None,
297          );
298      }
299      let new_seg_positions: Vec<_> = new_segs.iter().map(|s| segments[*s].at_y(y)).collect();
300      let padded_new_seg_positions: Vec<_> = adjust_x_positions(
301          &new_seg_positions,
302          padding / 2.0,
303          bbox.min_x() - padding,
304          bbox.max_x() + padding,
305      );
306      for ((&px, &x), seg_idx) in padded_new_seg_positions
307          .iter()
308          .zip(&new_seg_positions)
309          .zip(&new_segs)
310      {
311          let p = two_lines(
312              (bbox.min_x() + x, bbox.min_y() + y),
313              (bbox.min_x() + px, bbox.min_y() + y + padding / 2.0),
314              (bbox.min_x() + px, bbox.max_y()),
315          );
316          let color_idx = seg_color[seg_idx];
317          let c = path_color(color_idx);
318  
319          pixmap.stroke_path(
320              &p,
321              &color(c),
322              &thick_stroke,
323              tiny_skia::Transform::identity(),
324              None,
325          );
326      }
327  }
328  
329  fn generate_sweep_snapshot(path: PathBuf) -> Result<(), Failed> {
330      let input = std::fs::read_to_string(&path).unwrap();
331      let tree = usvg::Tree::from_str(&input, &usvg::Options::default()).unwrap();
332      let bezs = linesweeper_util::svg_to_bezpaths(&tree);
333      let bbox = linesweeper_util::bezier_bounding_box(bezs.iter());
334      let bez: BezPath = bezs
335          .into_iter()
336          .flat_map(|p| Affine::translate(-bbox.origin().to_vec2()) * p)
337          .collect();
338  
339      let mut segments = Segments::default();
340      segments.add_bez_path(&bez).unwrap();
341      segments.check_invariants();
342  
343      let eps = 16.0;
344      let mut range_bufs = SweepLineRangeBuffers::default();
345      let mut line_bufs = SweepLineBuffers::default();
346  
347      // Run through once just to count the ranges.
348      let mut sweep_state = Sweeper::new(&segments, eps);
349      let mut num_ranges = 0;
350      while let Some(mut line) = sweep_state.next_line(&mut line_bufs) {
351          while line.next_range(&mut range_bufs, &segments).is_some() {
352              num_ranges += 1;
353          }
354      }
355  
356      let pad = 32.0;
357      let mut sweep_state = Sweeper::new(&segments, eps);
358      let mut pixmap = Pixmap::new(
359          (bbox.width() + 2.0 * pad).ceil() as u32,
360          ((bbox.height() + pad) * num_ranges as f64 + pad).ceil() as u32,
361      )
362      .unwrap();
363  
364      let mut b = Rect::new(pad, pad, pad + bbox.width(), pad + bbox.height());
365      while let Some(mut line) = sweep_state.next_line(&mut line_bufs) {
366          while let Some(range) = line.next_range(&mut range_bufs, &segments) {
367              draw_orig_path(&mut pixmap, &bez, b.origin());
368              draw_sweep_line_range(&mut pixmap, &segments, range, b, pad);
369  
370              b = b + Vec2::new(0.0, bbox.height() + pad);
371          }
372      }
373  
374      let base_path = input_path_base(&path);
375      let out_path = output_path_for(base_path);
376      std::fs::create_dir_all(out_path.parent().unwrap()).unwrap();
377      pixmap.save_png(&out_path).unwrap();
378  
379      let new_image = kompari::load_image(&out_path)?;
380      let snapshot = kompari::load_image(&saved_snapshot_path_for(base_path))?;
381      match kompari::compare_images(&snapshot, &new_image) {
382          kompari::ImageDifference::None => Ok(()),
383          _ => Err("image comparison failed".into()),
384      }
385  }
386  
387  fn generate_position_snapshot(path: PathBuf) -> Result<(), Failed> {
388      let input = std::fs::read_to_string(&path).unwrap();
389      let tree = usvg::Tree::from_str(&input, &usvg::Options::default()).unwrap();
390      let bezs = linesweeper_util::svg_to_bezpaths(&tree);
391      let bbox = linesweeper_util::bezier_bounding_box(bezs.iter());
392      let bez: BezPath = bezs
393          .into_iter()
394          .flat_map(|p| Affine::translate(-bbox.origin().to_vec2()) * p)
395          .collect();
396  
397      let eps = 16.0;
398      let top = Topology::from_paths_binary(&bez, &BezPath::new(), eps).unwrap();
399      let out_paths = top.compute_positions();
400  
401      let pad = 2.0 * eps;
402      let bbox = top.bounding_box();
403      let mut pixmap = Pixmap::new(
404          (bbox.width() + 2.0 * pad).ceil() as u32,
405          (bbox.height() + 2.0 * pad).ceil() as u32,
406      )
407      .unwrap();
408      let pad_transform = tiny_skia::Transform::from_translate(
409          (pad - bbox.min_x()) as f32,
410          (pad - bbox.min_y()) as f32,
411      );
412  
413      let stroke = tiny_skia::Stroke {
414          width: 1.0,
415          ..Default::default()
416      };
417      for out_idx in top.segment_indices() {
418          let (path, far_idx) = &out_paths[out_idx];
419          for (idx, seg) in path.segments().enumerate() {
420              let skia_seg = skia_kurbo_seg(seg);
421  
422              let c = if far_idx == &Some(idx) {
423                  path_color(0)
424              } else {
425                  path_color(3)
426              };
427  
428              pixmap.stroke_path(&skia_seg, &color(c), &stroke, pad_transform, None);
429  
430              let p0 = seg.start();
431              let p0 = tiny_skia::PathBuilder::from_circle(p0.x as f32, p0.y as f32, 2.0).unwrap();
432              let p1 = seg.end();
433              let p1 = tiny_skia::PathBuilder::from_circle(p1.x as f32, p1.y as f32, 2.0).unwrap();
434              let black = color(tiny_skia::Color::BLACK);
435              pixmap.fill_path(
436                  &p0,
437                  &black,
438                  tiny_skia::FillRule::Winding,
439                  pad_transform,
440                  None,
441              );
442              pixmap.fill_path(
443                  &p1,
444                  &black,
445                  tiny_skia::FillRule::Winding,
446                  pad_transform,
447                  None,
448              );
449          }
450      }
451      let base_path = input_path_base(&path);
452      let out_path = output_path_for(base_path);
453      std::fs::create_dir_all(out_path.parent().unwrap()).unwrap();
454      pixmap.save_png(&out_path).unwrap();
455  
456      let new_image = kompari::load_image(&out_path)?;
457      let snapshot = kompari::load_image(&saved_snapshot_path_for(base_path))?;
458      match kompari::compare_images(&snapshot, &new_image) {
459          kompari::ImageDifference::None => Ok(()),
460          _ => Err("image comparison failed".into()),
461      }
462  }