use std::collections::VecDeque;
use kurbo::{BezPath, ParamCurve, Rect, Shape};
use crate::{
curve::{solve_t_for_y, y_subsegment, Order},
geom::Point,
order::ComparisonCache,
position::PositionedOutputSeg,
segments::{NonClosedPath, SegIdx, SegVec, Segments},
sweep::{
SegmentsConnectedAtX, SweepLineBuffers, SweepLineRange, SweepLineRangeBuffers, Sweeper,
},
};
pub trait WindingNumber:
Clone + std::fmt::Debug + std::ops::Add<Output = Self> + std::ops::AddAssign + Default + Eq
{
#[cfg(not(test))]
type Tag: Copy + std::fmt::Debug + Eq;
#[cfg(test)]
type Tag: Copy + std::fmt::Debug + Eq + serde::Serialize;
fn single(tag: Self::Tag, positive: bool) -> Self;
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, Eq, Default)]
pub struct BinaryWindingNumber {
pub shape_a: i32,
pub shape_b: i32,
}
impl WindingNumber for BinaryWindingNumber {
type Tag = bool;
fn single(tag: Self::Tag, positive: bool) -> Self {
let sign = if positive { 1 } else { -1 };
if tag {
Self {
shape_a: sign,
shape_b: 0,
}
} else {
Self {
shape_a: 0,
shape_b: sign,
}
}
}
}
impl std::ops::Add for BinaryWindingNumber {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
shape_a: self.shape_a + rhs.shape_a,
shape_b: self.shape_b + rhs.shape_b,
}
}
}
impl std::ops::AddAssign for BinaryWindingNumber {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs
}
}
impl std::fmt::Debug for BinaryWindingNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}a + {}b", self.shape_a, self.shape_b)
}
}
impl WindingNumber for i32 {
type Tag = ();
fn single((): Self::Tag, positive: bool) -> Self {
if positive {
1
} else {
-1
}
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, Eq, Default)]
pub struct HalfSegmentWindingNumbers<W: WindingNumber> {
pub counter_clockwise: W,
pub clockwise: W,
}
impl<W: WindingNumber> HalfSegmentWindingNumbers<W> {
fn is_trivial(&self) -> bool {
self.counter_clockwise == self.clockwise
}
}
impl<W: WindingNumber> std::fmt::Debug for HalfSegmentWindingNumbers<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cw {:?} | cc {:?}",
self.clockwise, self.counter_clockwise
)
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct OutputSegIdx(usize);
impl OutputSegIdx {
pub fn first_half(self) -> HalfOutputSegIdx {
HalfOutputSegIdx {
idx: self,
first_half: true,
}
}
pub fn second_half(self) -> HalfOutputSegIdx {
HalfOutputSegIdx {
idx: self,
first_half: false,
}
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct OutputSegVec<T> {
inner: Vec<T>,
}
impl_typed_vec!(OutputSegVec, OutputSegIdx, "o");
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct HalfOutputSegIdx {
idx: OutputSegIdx,
first_half: bool,
}
impl HalfOutputSegIdx {
pub fn other_half(self) -> Self {
Self {
idx: self.idx,
first_half: !self.first_half,
}
}
pub fn is_first_half(self) -> bool {
self.first_half
}
}
impl std::fmt::Debug for HalfOutputSegIdx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.first_half {
write!(f, "{:?}->", self.idx)
} else {
write!(f, "->{:?}", self.idx)
}
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct HalfOutputSegVec<T> {
start: Vec<T>,
end: Vec<T>,
}
impl<T> HalfOutputSegVec<T> {
pub fn with_capacity(cap: usize) -> Self {
Self {
start: Vec::with_capacity(cap),
end: Vec::with_capacity(cap),
}
}
}
impl<T: Default> HalfOutputSegVec<T> {
pub fn with_size(size: usize) -> Self {
Self {
start: std::iter::from_fn(|| Some(T::default()))
.take(size)
.collect(),
end: std::iter::from_fn(|| Some(T::default()))
.take(size)
.collect(),
}
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for HalfOutputSegVec<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
struct Entry<'a, T> {
idx: usize,
start: &'a T,
end: &'a T,
}
impl<T: std::fmt::Debug> std::fmt::Debug for Entry<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{idx:4}: {start:?} -> {end:?}",
idx = self.idx,
start = self.start,
end = self.end
)
}
}
let mut list = f.debug_list();
for (idx, (start, end)) in self.start.iter().zip(&self.end).enumerate() {
list.entry(&Entry { idx, start, end });
}
list.finish()
}
}
impl<T> Default for HalfOutputSegVec<T> {
fn default() -> Self {
Self {
start: Vec::new(),
end: Vec::new(),
}
}
}
impl<T> std::ops::Index<HalfOutputSegIdx> for HalfOutputSegVec<T> {
type Output = T;
fn index(&self, index: HalfOutputSegIdx) -> &Self::Output {
if index.first_half {
&self.start[index.idx.0]
} else {
&self.end[index.idx.0]
}
}
}
impl<T> std::ops::IndexMut<HalfOutputSegIdx> for HalfOutputSegVec<T> {
fn index_mut(&mut self, index: HalfOutputSegIdx) -> &mut T {
if index.first_half {
&mut self.start[index.idx.0]
} else {
&mut self.end[index.idx.0]
}
}
}
impl<T> std::ops::Index<HalfOutputSegIdx> for OutputSegVec<T> {
type Output = T;
fn index(&self, index: HalfOutputSegIdx) -> &Self::Output {
&self.inner[index.idx.0]
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub struct PointIdx(usize);
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Hash, PartialEq, Eq)]
struct PointVec<T> {
inner: Vec<T>,
}
impl_typed_vec!(PointVec, PointIdx, "p");
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
struct PointNeighbors {
clockwise: HalfOutputSegIdx,
counter_clockwise: HalfOutputSegIdx,
}
impl std::fmt::Debug for PointNeighbors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?} o {:?}", self.counter_clockwise, self.clockwise)
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Debug)]
pub struct Topology<W: WindingNumber> {
eps: f64,
tag: SegVec<W::Tag>,
open_segs: SegVec<VecDeque<OutputSegIdx>>,
winding: OutputSegVec<HalfSegmentWindingNumbers<W>>,
points: PointVec<Point>,
point_idx: HalfOutputSegVec<PointIdx>,
point_neighbors: HalfOutputSegVec<PointNeighbors>,
deleted: OutputSegVec<bool>,
scan_west: OutputSegVec<Option<OutputSegIdx>>,
scan_east: OutputSegVec<Option<OutputSegIdx>>,
scan_after: Vec<(f64, Option<OutputSegIdx>, Option<OutputSegIdx>)>,
pub orig_seg: OutputSegVec<SegIdx>,
#[cfg_attr(test, serde(skip))]
pub segments: Segments,
positively_oriented: OutputSegVec<bool>,
}
impl<W: WindingNumber> Topology<W> {
pub fn from_paths<'a, Iter>(paths: Iter, eps: f64) -> Result<Self, NonClosedPath>
where
Iter: IntoIterator<Item = (&'a BezPath, W::Tag)>,
{
let mut segments = Segments::default();
let mut tag = Vec::new();
for (p, t) in paths {
segments.add_path_without_updating_enter_exit(p, true)?;
tag.resize(segments.len(), t);
}
segments.update_enter_exit(0);
segments.check_invariants();
Ok(Self::from_segments(segments, SegVec::from_vec(tag), eps))
}
fn from_segments(segments: Segments, tag: SegVec<W::Tag>, eps: f64) -> Self {
let mut ret = Self {
eps,
tag,
open_segs: SegVec::with_size(segments.len()),
winding: OutputSegVec::with_capacity(segments.len()),
points: PointVec::with_capacity(segments.len()),
point_idx: HalfOutputSegVec::with_capacity(segments.len()),
point_neighbors: HalfOutputSegVec::with_capacity(segments.len()),
deleted: OutputSegVec::with_capacity(segments.len()),
scan_west: OutputSegVec::with_capacity(segments.len()),
scan_east: OutputSegVec::with_capacity(segments.len()),
scan_after: Vec::new(),
orig_seg: OutputSegVec::with_capacity(segments.len()),
positively_oriented: OutputSegVec::with_capacity(segments.len()),
segments: Segments::default(),
};
let mut sweep_state = Sweeper::new(&segments, eps);
let mut range_bufs = SweepLineRangeBuffers::default();
let mut line_bufs = SweepLineBuffers::default();
while let Some(mut line) = sweep_state.next_line(&mut line_bufs) {
while let Some(positions) = line.next_range(&mut range_bufs, &segments) {
let range = positions.seg_range();
let scan_west_seg = if range.segs.start == 0 {
None
} else {
let prev_seg = positions.line().line_segment(range.segs.start - 1).unwrap();
debug_assert!(!ret.open_segs[prev_seg].is_empty());
ret.open_segs[prev_seg].front().copied()
};
ret.process_sweep_line_range(positions, &segments, scan_west_seg);
}
}
ret.segments = segments;
#[cfg(feature = "slow-asserts")]
ret.check_invariants();
ret.merge_coincident();
#[cfg(feature = "slow-asserts")]
ret.check_invariants();
ret
}
fn add_segs_clockwise(
&mut self,
first_seg: &mut Option<HalfOutputSegIdx>,
last_seg: &mut Option<HalfOutputSegIdx>,
segs: impl Iterator<Item = HalfOutputSegIdx>,
p: PointIdx,
) {
for seg in segs {
self.point_idx[seg] = p;
if first_seg.is_none() {
*first_seg = Some(seg);
}
if let Some(last) = last_seg {
self.point_neighbors[*last].clockwise = seg;
self.point_neighbors[seg].counter_clockwise = *last;
}
*last_seg = Some(seg);
}
if let Some((first, last)) = first_seg.zip(*last_seg) {
self.point_neighbors[last].clockwise = first;
self.point_neighbors[first].counter_clockwise = last;
}
}
fn add_segs_counter_clockwise(
&mut self,
first_seg: &mut Option<HalfOutputSegIdx>,
last_seg: &mut Option<HalfOutputSegIdx>,
segs: impl Iterator<Item = HalfOutputSegIdx>,
p: PointIdx,
) {
for seg in segs {
self.point_idx[seg] = p;
if last_seg.is_none() {
*last_seg = Some(seg);
}
if let Some(first) = first_seg {
self.point_neighbors[*first].counter_clockwise = seg;
self.point_neighbors[seg].clockwise = *first;
}
*first_seg = Some(seg);
}
if let Some((first, last)) = first_seg.zip(*last_seg) {
self.point_neighbors[last].clockwise = first;
self.point_neighbors[first].counter_clockwise = last;
}
}
fn second_half_segs<'a, 'slf: 'a>(
&'slf mut self,
segs: impl Iterator<Item = SegIdx> + 'a,
) -> impl Iterator<Item = HalfOutputSegIdx> + 'a {
segs.map(|s| {
self.open_segs[s]
.pop_front()
.expect("should be open")
.second_half()
})
}
fn new_half_seg(
&mut self,
idx: SegIdx,
p: PointIdx,
winding: HalfSegmentWindingNumbers<W>,
horizontal: bool,
positively_oriented: bool,
) -> OutputSegIdx {
let out_idx = OutputSegIdx(self.winding.inner.len());
if horizontal {
self.open_segs[idx].push_front(out_idx);
} else {
self.open_segs[idx].push_back(out_idx);
}
self.point_idx.start.push(p);
self.point_idx
.end
.push(PointIdx(usize::MAX));
let no_nbrs = PointNeighbors {
clockwise: out_idx.first_half(),
counter_clockwise: out_idx.first_half(),
};
self.point_neighbors.start.push(no_nbrs);
self.point_neighbors.end.push(no_nbrs);
self.winding.inner.push(winding);
self.deleted.inner.push(false);
self.scan_west.inner.push(None);
self.scan_east.inner.push(None);
self.orig_seg.inner.push(idx);
self.positively_oriented.inner.push(positively_oriented);
out_idx
}
fn process_sweep_line_range(
&mut self,
mut pos: SweepLineRange,
segments: &Segments,
mut scan_west: Option<OutputSegIdx>,
) {
let y = pos.line().y();
let mut winding = scan_west
.map(|idx| self.winding[idx].counter_clockwise.clone())
.unwrap_or_default();
let mut seg_buf = Vec::new();
let mut connected_segs = SegmentsConnectedAtX::default();
let mut last_connected_down_seg: Option<OutputSegIdx> = None;
while let Some(next_x) = pos.x() {
let p = PointIdx(self.points.inner.len());
self.points.inner.push(Point::new(next_x, y));
let mut first_seg = None;
let mut last_seg = None;
let hsegs = pos.active_horizontals();
seg_buf.clear();
seg_buf.extend(self.second_half_segs(hsegs));
self.add_segs_clockwise(&mut first_seg, &mut last_seg, seg_buf.iter().copied(), p);
pos.update_segments_at_x(&mut connected_segs);
seg_buf.clear();
seg_buf.extend(self.second_half_segs(connected_segs.connected_up()));
self.add_segs_clockwise(&mut first_seg, &mut last_seg, seg_buf.iter().copied(), p);
seg_buf.clear();
for new_seg in connected_segs.connected_down() {
let prev_winding = winding.clone();
let orientation = segments.positively_oriented(new_seg);
winding += W::single(self.tag[new_seg], orientation);
let windings = HalfSegmentWindingNumbers {
clockwise: prev_winding,
counter_clockwise: winding.clone(),
};
let half_seg = self.new_half_seg(new_seg, p, windings, false, orientation);
self.scan_west[half_seg] = scan_west;
scan_west = Some(half_seg);
seg_buf.push(half_seg.first_half());
last_connected_down_seg = Some(half_seg);
}
self.add_segs_counter_clockwise(
&mut first_seg,
&mut last_seg,
seg_buf.iter().copied(),
p,
);
pos.increase_x();
let hsegs = pos.active_horizontals_and_orientations();
let mut w = winding.clone();
seg_buf.clear();
for (new_seg, same_orientation) in hsegs {
let prev_w = w.clone();
let orientation = same_orientation == segments.positively_oriented(new_seg);
w += W::single(self.tag[new_seg], orientation);
let windings = HalfSegmentWindingNumbers {
counter_clockwise: w.clone(),
clockwise: prev_w,
};
let half_seg = self.new_half_seg(new_seg, p, windings, true, orientation);
self.scan_west[half_seg] = scan_west;
seg_buf.push(half_seg.first_half());
}
self.add_segs_counter_clockwise(
&mut first_seg,
&mut last_seg,
seg_buf.iter().copied(),
p,
);
}
if let Some(seg) = last_connected_down_seg {
if let Some(east_nbr) = pos.line().line_entry(pos.seg_range().segs.end) {
if !east_nbr.is_in_changed_interval() {
self.scan_east[seg] = self.open_segs[east_nbr.seg].front().copied()
}
}
}
if last_connected_down_seg.is_none() {
let seg_range = pos.seg_range().segs;
let west_nbr = seg_range
.start
.checked_sub(1)
.and_then(|idx| pos.line().line_segment(idx));
let east_nbr = pos.line().line_segment(pos.seg_range().segs.end);
let west = west_nbr.map(|idx| *self.open_segs[idx].front().unwrap());
let east = east_nbr.map(|idx| *self.open_segs[idx].front().unwrap());
if west.is_some() || east.is_some() {
self.scan_after.push((y, west, east));
}
}
}
fn delete_half(&mut self, half_seg: HalfOutputSegIdx) {
let nbr = self.point_neighbors[half_seg];
self.point_neighbors[nbr.clockwise].counter_clockwise = nbr.counter_clockwise;
self.point_neighbors[nbr.counter_clockwise].clockwise = nbr.clockwise;
}
fn delete(&mut self, seg: OutputSegIdx) {
self.deleted[seg] = true;
self.delete_half(seg.first_half());
self.delete_half(seg.second_half());
}
fn is_horizontal(&self, seg: OutputSegIdx) -> bool {
self.point(seg.first_half()).y == self.point(seg.second_half()).y
}
fn merge_coincident(&mut self) {
for idx in 0..self.winding.inner.len() {
let idx = OutputSegIdx(idx);
if self.deleted[idx] {
continue;
}
let cw_nbr = self.point_neighbors[idx.first_half()].clockwise;
if self.point_idx[idx.second_half()] == self.point_idx[cw_nbr.other_half()] {
let y0 = self.point(idx.first_half()).y;
let y1 = self.point(idx.second_half()).y;
if y0 != y1 && self.scan_west[idx] != Some(cw_nbr.idx) {
continue;
}
if y0 != y1 {
let s1 = self.orig_seg[idx];
let s2 = self.orig_seg[cw_nbr];
let s1 = y_subsegment(self.segments[s1].to_kurbo_cubic(), y0, y1);
let s2 = y_subsegment(self.segments[s2].to_kurbo_cubic(), y0, y1);
if (s1.p0 - s2.p0).hypot() > self.eps
|| (s1.p1 - s2.p1).hypot() > self.eps
|| (s1.p2 - s2.p2).hypot() > self.eps
|| (s1.p3 - s2.p3).hypot() > self.eps
{
continue;
}
}
debug_assert!(cw_nbr.first_half);
self.delete(idx);
self.winding[cw_nbr.idx].counter_clockwise =
self.winding[idx].counter_clockwise.clone();
if self.winding[cw_nbr.idx].is_trivial() {
self.delete(cw_nbr.idx);
}
}
}
}
pub fn segment_indices(&self) -> impl Iterator<Item = OutputSegIdx> + '_ {
(0..self.winding.inner.len())
.map(OutputSegIdx)
.filter(|i| !self.deleted[*i])
}
pub fn winding_clockwise(&self, idx: HalfOutputSegIdx) -> &W {
if idx.first_half {
&self.winding[idx.idx].clockwise
} else {
&self.winding[idx.idx].counter_clockwise
}
}
pub fn winding_counter_clockwise(&self, idx: HalfOutputSegIdx) -> &W {
if idx.first_half {
&self.winding[idx.idx].counter_clockwise
} else {
&self.winding[idx.idx].clockwise
}
}
pub fn point(&self, idx: HalfOutputSegIdx) -> &Point {
&self.points[self.point_idx[idx]]
}
fn segs_to_path(
&self,
segs: &[HalfOutputSegIdx],
positions: &OutputSegVec<PositionedOutputSeg>,
) -> BezPath {
let mut ret = BezPath::default();
let mut unfinished: Option<f64> = None;
let start_idx = segs
.iter()
.position(|s| self.can_merge(s.other_half(), positions).is_none())
.unwrap();
ret.move_to(self.point(segs[start_idx].other_half()).to_kurbo());
for seg in segs[start_idx..].iter().chain(&segs[..start_idx]) {
let path = &positions[seg.idx];
let rev_path;
let oriented_path = if seg.is_first_half() {
rev_path = path.path.reverse_subpaths();
&rev_path
} else {
&path.path
};
let mut elems = oriented_path.iter().skip(1);
if let (Some(our_t), Some(unfinished_t)) =
(self.can_merge(seg.other_half(), positions), unfinished)
{
let input = self.segments.input_segs[self.orig_seg[seg.idx]];
let sub_input = if our_t > unfinished_t {
input.subsegment(unfinished_t..our_t)
} else {
input
.reverse()
.subsegment((1.0 - unfinished_t)..(1.0 - our_t))
};
*ret.elements_mut().last_mut().unwrap() = sub_input.as_path_el();
elems.next();
if self.can_merge(*seg, positions).is_none() {
unfinished = None;
}
} else if let Some(t) = self.can_merge(*seg, positions) {
unfinished = Some(t);
} else {
unfinished = None;
}
ret.extend(elems);
}
ret.close_path();
ret
}
fn is_simple_point(&self, idx: HalfOutputSegIdx) -> bool {
self.point_neighbors[idx].clockwise == self.point_neighbors[idx].counter_clockwise
}
fn can_merge(
&self,
seg: HalfOutputSegIdx,
positions: &OutputSegVec<PositionedOutputSeg>,
) -> Option<f64> {
let pos = &positions[seg.idx];
let orig_idx = self.orig_seg[seg.idx];
if self.segments[orig_idx].is_horizontal() {
return None;
}
let extremal_seg_is_copied = if seg.is_first_half() {
pos.copied_idx == Some(0)
} else {
pos.copied_idx == Some(pos.path.elements().len() - 2)
};
let has_orig_orientation =
seg.is_first_half() == self.segments.positively_oriented(orig_idx);
let start_y = self.point(seg).y;
let end_y = self.point(seg.other_half()).y;
let split_from_pred = self.segments.split_from_predecessor[orig_idx];
let can_merge = if has_orig_orientation {
let seg_in_input_t = split_from_pred.0;
if (seg_in_input_t > 0.0)
&& extremal_seg_is_copied
&& start_y == self.segments.oriented_start(orig_idx).y
{
let t = solve_t_for_y(self.segments[orig_idx].to_kurbo_cubic(), end_y);
Some(self.input_seg_t(seg.idx, t))
} else {
None
}
} else {
let seg_in_input_t = split_from_pred.1;
if seg_in_input_t < 1.0
&& extremal_seg_is_copied
&& start_y == self.segments.oriented_end(orig_idx).y
{
let t = solve_t_for_y(self.segments[orig_idx].to_kurbo_cubic(), end_y);
Some(self.input_seg_t(seg.idx, t))
} else {
None
}
};
if self.is_simple_point(seg) {
can_merge
} else {
None
}
}
fn input_seg_t(&self, out_seg: OutputSegIdx, t: f64) -> f64 {
let orig_idx = self.orig_seg[out_seg];
let split_from_pred = self.segments.split_from_predecessor[orig_idx];
let ret = if self.segments.positively_oriented(orig_idx) {
split_from_pred.0 + t * (split_from_pred.1 - split_from_pred.0)
} else {
split_from_pred.1 - t * (split_from_pred.1 - split_from_pred.0)
};
ret.clamp(0.0, 1.0)
}
pub fn contours(&self, inside: impl Fn(&W) -> bool) -> Contours {
let mut ret = Contours::default();
let mut seg_contour: Vec<Option<ContourIdx>> = vec![None; self.winding.inner.len()];
let positions = self.compute_positions();
let bdy = |idx: OutputSegIdx| -> bool {
inside(&self.winding[idx].clockwise) != inside(&self.winding[idx].counter_clockwise)
};
let mut visited = vec![false; self.winding.inner.len()];
let mut last_visit = PointVec::with_capacity(self.points.inner.len());
last_visit.inner.resize(self.points.inner.len(), None);
for idx in self.segment_indices() {
if visited[idx.0] {
continue;
}
if !bdy(idx) {
continue;
}
let contour_idx = ContourIdx(ret.contours.len());
let mut contour = Contour::default();
let mut west_seg = self.scan_west[idx];
while let Some(left) = west_seg {
if self.deleted[left] || !bdy(left) {
west_seg = self.scan_west[left];
} else {
break;
}
}
if let Some(west) = west_seg {
if let Some(west_contour) = seg_contour[west.0] {
let outside = !inside(self.winding_counter_clockwise(west.first_half()));
if outside == ret.contours[west_contour.0].outer {
contour.parent = ret.contours[west_contour.0].parent;
contour.outer = outside;
debug_assert!(outside || contour.parent.is_some());
} else {
contour.parent = Some(west_contour);
contour.outer = !ret.contours[west_contour.0].outer;
}
} else {
panic!("I'm {idx:?}, west is {west:?}. Y u no have contour?");
}
};
ret.contours.push(contour);
let (start, mut next) = if inside(&self.winding[idx].counter_clockwise) {
(idx.first_half(), idx.second_half())
} else {
(idx.second_half(), idx.first_half())
};
let mut segs = Vec::new();
last_visit[self.point_idx[start]] = Some(0);
debug_assert!(inside(self.winding_counter_clockwise(start)));
loop {
visited[next.idx.0] = true;
debug_assert!(inside(self.winding_clockwise(next)));
debug_assert!(!inside(self.winding_counter_clockwise(next)));
let mut nbr = self.point_neighbors[next].clockwise;
debug_assert!(inside(self.winding_counter_clockwise(nbr)));
while inside(self.winding_clockwise(nbr)) {
nbr = self.point_neighbors[nbr].clockwise;
}
segs.push(next);
if nbr == start {
break;
}
let p = self.point_idx[nbr];
let last_visit_idx = last_visit[p]
.filter(|&idx| idx < segs.len() && self.point_idx[segs[idx].other_half()] == p);
if let Some(seg_idx) = last_visit_idx {
debug_assert_eq!(self.point_idx[segs[seg_idx].other_half()], p);
let loop_contour_idx = ContourIdx(ret.contours.len());
for &seg in &segs[seg_idx..] {
seg_contour[seg.idx.0] = Some(loop_contour_idx);
}
ret.contours.push(Contour {
path: self.segs_to_path(&segs[seg_idx..], &positions),
parent: Some(contour_idx),
outer: !ret.contours[contour_idx.0].outer,
});
segs.truncate(seg_idx);
} else {
last_visit[p] = Some(segs.len());
}
next = nbr.other_half();
}
for &seg in &segs {
seg_contour[seg.idx.0] = Some(contour_idx);
}
ret.contours[contour_idx.0].path = self.segs_to_path(&segs, &positions);
}
ret
}
pub fn bounding_box(&self) -> kurbo::Rect {
let mut rect = Rect::new(
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
for seg in self.segments.segments() {
rect = rect.union(seg.to_kurbo_cubic().bounding_box());
}
rect
}
fn build_scan_line_orders(&self) -> ScanLineOrder {
let mut west_map: OutputSegVec<Vec<(f64, Option<OutputSegIdx>)>> =
OutputSegVec::with_size(self.winding.len());
let mut east_map: OutputSegVec<Vec<(f64, Option<OutputSegIdx>)>> =
OutputSegVec::with_size(self.winding.len());
for idx in self.scan_west.indices().filter(|i| !self.is_horizontal(*i)) {
let y = self.point(idx.first_half()).y;
if let Some(west) = self.scan_west[idx] {
west_map[idx].push((y, Some(west)));
}
}
for idx in self.scan_east.indices() {
let y = self.point(idx.first_half()).y;
if let Some(east) = self.scan_east[idx] {
east_map[idx].push((y, Some(east)));
if let Some((last_y, _)) = west_map[east].last() {
debug_assert!(y > *last_y);
}
west_map[east].push((y, Some(idx)));
}
}
for idx in self.scan_west.indices().filter(|i| !self.is_horizontal(*i)) {
let y = self.point(idx.first_half()).y;
if let Some(west) = self.scan_west[idx] {
if let Some((last_y, _)) = east_map[west].last() {
debug_assert!(y > *last_y);
}
east_map[west].push((y, Some(idx)));
}
}
for &(y, west, east) in &self.scan_after {
if let Some(east) = east {
west_map[east].push((y, west));
}
if let Some(west) = west {
east_map[west].push((y, east));
}
}
for vec in &mut west_map.inner {
vec.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
vec.dedup();
}
for vec in &mut east_map.inner {
vec.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
vec.dedup();
}
let ret = ScanLineOrder::new(west_map, east_map);
#[cfg(feature = "slow-asserts")]
ret.check_invariants(&self.orig_seg, &self.segments);
ret
}
pub fn compute_positions(&self) -> OutputSegVec<PositionedOutputSeg> {
let mut cmp = ComparisonCache::new(self.eps, self.eps / 2.0);
let mut endpoints = HalfOutputSegVec::with_size(self.orig_seg.len());
for idx in self.orig_seg.indices() {
endpoints[idx.first_half()] = self.points[self.point_idx[idx.first_half()]].to_kurbo();
endpoints[idx.second_half()] =
self.points[self.point_idx[idx.second_half()]].to_kurbo();
}
crate::position::compute_positions(
&self.segments,
&self.orig_seg,
&mut cmp,
&endpoints,
&self.build_scan_line_orders(),
self.eps / 4.0,
)
}
#[cfg(feature = "slow-asserts")]
fn check_invariants(&self) {
for out_idx in self.segment_indices() {
if !self.deleted[out_idx] {
for half in [out_idx.first_half(), out_idx.second_half()] {
let cw_nbr = self.point_neighbors[half].clockwise;
if self.winding_clockwise(half) != self.winding_counter_clockwise(cw_nbr) {
#[cfg(feature = "debug-svg")]
{
dbg!(self);
let svg = self.dump_svg(|_| "black".to_owned());
svg::save("out.svg", &svg).unwrap();
}
dbg!(half, cw_nbr);
panic!();
}
}
}
}
let mut out_segs = SegVec::<Vec<OutputSegIdx>>::with_size(self.segments.len());
for out_seg in self.winding.indices() {
out_segs[self.orig_seg[out_seg]].push(out_seg);
}
let mut realized_endpoints = Vec::new();
for (in_seg, out_segs) in out_segs.iter_mut() {
out_segs.sort_by(|&o1, &o2| {
let p11 = self.point(o1.first_half());
let p12 = self.point(o1.second_half());
let p21 = self.point(o2.first_half());
let p22 = self.point(o2.second_half());
let horiz1 = p11.y == p12.y;
let horiz2 = p21.y == p22.y;
let cmp = (p11.y, p12.y).partial_cmp(&(p21.y, p22.y)).unwrap();
let cmp = cmp.then((p11.x, p12.x).partial_cmp(&(p21.x, p22.x)).unwrap());
if horiz1 && horiz2 {
debug_assert_eq!(self.positively_oriented[o1], self.positively_oriented[o2]);
if self.positively_oriented[o1] == self.segments.positively_oriented(in_seg) {
cmp
} else {
cmp.reverse()
}
} else {
cmp
}
});
let mut first = None;
let mut last = None;
for &out_seg in &*out_segs {
let same_orientation =
self.positively_oriented[out_seg] == self.segments.positively_oriented(in_seg);
let (first_endpoint, second_endpoint) = if same_orientation {
(out_seg.first_half(), out_seg.second_half())
} else {
(out_seg.second_half(), out_seg.first_half())
};
if first.is_none() {
first = Some(first_endpoint);
}
if let Some(last) = last {
assert_eq!(self.point_idx[first_endpoint], self.point_idx[last]);
}
last = Some(second_endpoint);
}
let first = first.unwrap();
let last = last.unwrap();
let (first, last) = if self.segments.positively_oriented(in_seg) {
(first, last)
} else {
(last, first)
};
realized_endpoints.push((self.point_idx[first], self.point_idx[last]));
}
for seg in self.segments.indices() {
if let Some(prev) = self.segments.contour_prev(seg) {
assert_eq!(realized_endpoints[prev.0].1, realized_endpoints[seg.0].0);
}
}
}
#[cfg(feature = "debug-svg")]
pub fn dump_svg(&self, tag_color: impl Fn(W::Tag) -> String) -> svg::Document {
let mut bbox = Rect::new(
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
let mut document = svg::Document::new();
let p = |point: Point| (point.x, point.y);
for seg in self.segment_indices() {
let p0 = p(*self.point(seg.first_half()));
let p1 = p(*self.point(seg.second_half()));
bbox = bbox.union_pt(p0);
bbox = bbox.union_pt(p1);
}
bbox = bbox.inset(2.0);
let bbox_size = bbox.width().max(bbox.height());
let stroke_width = 1.0 * bbox_size / 1024.0;
let point_radius = 1.5 * bbox_size / 1024.0;
let font_size = 8.0 * bbox_size / 1024.0;
for seg in self.segment_indices() {
let mut data = svg::node::element::path::Data::new();
let p0 = p(*self.point(seg.first_half()));
let p1 = p(*self.point(seg.second_half()));
data = data.move_to(p0);
data = data.line_to(p1);
let color = tag_color(self.tag[self.orig_seg[seg]]);
let path = svg::node::element::Path::new()
.set("id", format!("{seg:?}"))
.set("class", format!("{:?}", self.orig_seg[seg]))
.set("stroke", color)
.set("stroke-width", stroke_width)
.set("stroke-linecap", "round")
.set("stroke-linejoin", "round")
.set("opacity", 0.2)
.set("fill", "none")
.set("d", data);
document = document.add(path);
let text = svg::node::element::Text::new(format!("{seg:?}",))
.set("font-size", font_size)
.set("text-anchor", "middle")
.set("x", (p0.0 + p1.0) / 2.0)
.set("y", (p0.1 + p1.1) / 2.0);
document = document.add(text);
}
for p_idx in self.points.indices() {
let p = self.points[p_idx];
let c = svg::node::element::Circle::new()
.set("id", format!("{p_idx:?}"))
.set("cx", p.x)
.set("cy", p.y)
.set("r", point_radius)
.set("stroke", "none")
.set("fill", "black");
document = document.add(c);
}
document = document.set(
"viewBox",
(bbox.min_x(), bbox.min_y(), bbox.width(), bbox.height()),
);
document
}
}
impl Topology<i32> {
pub fn from_path(path: &BezPath, eps: f64) -> Result<Self, NonClosedPath> {
Self::from_paths(std::iter::once((path, ())), eps)
}
}
impl Topology<BinaryWindingNumber> {
pub fn from_polylines_binary(
set_a: impl IntoIterator<Item = impl IntoIterator<Item = Point>>,
set_b: impl IntoIterator<Item = impl IntoIterator<Item = Point>>,
eps: f64,
) -> Self {
let mut segments = Segments::default();
let mut shape_a = Vec::new();
segments.add_closed_polylines(set_a);
shape_a.resize(segments.len(), true);
segments.add_closed_polylines(set_b);
shape_a.resize(segments.len(), false);
Self::from_segments(segments, SegVec::from_vec(shape_a), eps)
}
pub fn from_paths_binary(
set_a: &BezPath,
set_b: &BezPath,
eps: f64,
) -> Result<Self, NonClosedPath> {
Self::from_paths([(set_a, true), (set_b, false)], eps)
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Debug)]
struct HalfScanLineOrder {
inner: OutputSegVec<Vec<(f64, Option<OutputSegIdx>)>>,
}
impl HalfScanLineOrder {
fn neighbor_after(&self, seg: OutputSegIdx, y: f64) -> Option<OutputSegIdx> {
self.iter(seg)
.take_while(|(y0, _, _)| *y0 <= y)
.find(|(_, y1, _)| *y1 > y)
.and_then(|(_, _, idx)| idx)
}
fn iter(
&self,
seg: OutputSegIdx,
) -> impl Iterator<Item = (f64, f64, Option<OutputSegIdx>)> + '_ {
let ends = self.inner[seg]
.iter()
.map(|(y0, _)| *y0)
.skip(1)
.chain(std::iter::once(f64::INFINITY));
self.inner[seg]
.iter()
.zip(ends)
.map(|((y0, maybe_seg), y1)| (*y0, y1, *maybe_seg))
}
fn close_neighbor_height_after(
&self,
seg: OutputSegIdx,
y: f64,
orig_seg: &OutputSegVec<SegIdx>,
segs: &Segments,
cmp: &mut ComparisonCache,
) -> Option<f64> {
for (_, y1, other_seg) in self.iter(seg) {
if y1 <= y {
continue;
}
if let Some(other_seg) = other_seg {
let order = cmp.compare_segments(segs, orig_seg[seg], orig_seg[other_seg]);
let next_ish = order
.iter()
.take_while(|(order_y0, _, _)| *order_y0 < y1)
.filter(|(_, _, order)| *order == Order::Ish)
.find(|(order_y0, _, _)| *order_y0 >= y);
if let Some((order_y0, _, _)) = next_ish {
return Some(order_y0);
}
}
}
None
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Debug)]
pub struct ScanLineOrder {
west_map: HalfScanLineOrder,
east_map: HalfScanLineOrder,
}
impl ScanLineOrder {
fn new(
west: OutputSegVec<Vec<(f64, Option<OutputSegIdx>)>>,
east: OutputSegVec<Vec<(f64, Option<OutputSegIdx>)>>,
) -> Self {
Self {
west_map: HalfScanLineOrder { inner: west },
east_map: HalfScanLineOrder { inner: east },
}
}
pub fn west_neighbor_after(&self, seg: OutputSegIdx, y: f64) -> Option<OutputSegIdx> {
self.west_map.neighbor_after(seg, y)
}
pub fn east_neighbor_after(&self, seg: OutputSegIdx, y: f64) -> Option<OutputSegIdx> {
self.east_map.neighbor_after(seg, y)
}
pub fn close_east_neighbor_height_after(
&self,
seg: OutputSegIdx,
y: f64,
orig_seg: &OutputSegVec<SegIdx>,
segs: &Segments,
cmp: &mut ComparisonCache,
) -> Option<f64> {
self.east_map
.close_neighbor_height_after(seg, y, orig_seg, segs, cmp)
}
pub fn close_west_neighbor_height_after(
&self,
seg: OutputSegIdx,
y: f64,
orig_seg: &OutputSegVec<SegIdx>,
segs: &Segments,
cmp: &mut ComparisonCache,
) -> Option<f64> {
self.west_map
.close_neighbor_height_after(seg, y, orig_seg, segs, cmp)
}
#[cfg(feature = "slow-asserts")]
fn check_invariants(&self, orig_seg: &OutputSegVec<SegIdx>, segs: &Segments) {
for idx in self.east_map.inner.indices() {
for &(y, east_idx) in &self.east_map.inner[idx] {
if let Some(east_idx) = east_idx {
let seg = &segs[orig_seg[idx]];
let east_seg = &segs[orig_seg[east_idx]];
assert!(y >= seg.start().y && y >= east_seg.start().y);
assert!(y < seg.end().y && y < east_seg.end().y);
}
}
for &(y, west_idx) in &self.west_map.inner[idx] {
if let Some(west_idx) = west_idx {
let seg = &segs[orig_seg[idx]];
let west_seg = &segs[orig_seg[west_idx]];
assert!(y >= seg.start().y && y >= west_seg.start().y);
assert!(y < seg.end().y && y < west_seg.end().y);
}
}
}
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct ContourIdx(pub usize);
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Debug)]
pub struct Contour {
pub path: BezPath,
pub parent: Option<ContourIdx>,
pub outer: bool,
}
impl Default for Contour {
fn default() -> Self {
Self {
path: BezPath::default(),
outer: true,
parent: None,
}
}
}
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Debug, Default)]
pub struct Contours {
contours: Vec<Contour>,
}
impl Contours {
pub fn grouped(&self) -> Vec<Vec<ContourIdx>> {
let mut children = vec![Vec::new(); self.contours.len()];
let mut top_level = Vec::new();
for i in 0..self.contours.len() {
if let Some(parent) = self.contours[i].parent {
children[parent.0].push(ContourIdx(i));
} else {
top_level.push(ContourIdx(i));
}
}
let mut ret = Vec::with_capacity(top_level.len());
for top in top_level {
let mut tree = Vec::new();
fn visit(idx: ContourIdx, children: &[Vec<ContourIdx>], acc: &mut Vec<ContourIdx>) {
acc.push(idx);
for &child in &children[idx.0] {
visit(child, children, acc);
}
}
visit(top, &children, &mut tree);
ret.push(tree);
}
ret
}
pub fn contours(&self) -> impl Iterator<Item = &Contour> + '_ {
self.contours.iter()
}
}
impl std::ops::Index<ContourIdx> for Contours {
type Output = Contour;
fn index(&self, index: ContourIdx) -> &Self::Output {
&self.contours[index.0]
}
}
#[cfg(test)]
mod tests {
use kurbo::BezPath;
use proptest::prelude::*;
use crate::{
geom::Point,
order::ComparisonCache,
perturbation::{
f64_perturbation, perturbation, realize_perturbation, F64Perturbation, Perturbation,
},
SegIdx,
};
use super::Topology;
fn p(x: f64, y: f64) -> Point {
Point::new(x, y)
}
const EMPTY: [[Point; 0]; 0] = [];
#[test]
fn square() {
let segs = [[p(0.0, 0.0), p(1.0, 0.0), p(1.0, 1.0), p(0.0, 1.0)]];
let eps = 0.01;
let top = Topology::from_polylines_binary(segs, EMPTY, eps);
insta::assert_ron_snapshot!(top);
}
#[test]
fn diamond() {
let segs = [[p(0.0, 0.0), p(1.0, 1.0), p(0.0, 2.0), p(-1.0, 1.0)]];
let eps = 0.01;
let top = Topology::from_polylines_binary(segs, EMPTY, eps);
insta::assert_ron_snapshot!(top);
}
#[test]
fn square_and_diamond() {
let square = [[p(0.0, 0.0), p(1.0, 0.0), p(1.0, 1.0), p(0.0, 1.0)]];
let diamond = [[p(0.0, 0.0), p(1.0, 1.0), p(0.0, 2.0), p(-1.0, 1.0)]];
let eps = 0.01;
let top = Topology::from_polylines_binary(square, diamond, eps);
insta::assert_ron_snapshot!(top);
}
#[test]
fn square_with_double_back() {
let segs = [[
p(0.0, 0.0),
p(0.5, 0.0),
p(0.5, 0.5),
p(0.5, 0.0),
p(1.0, 0.0),
p(1.0, 1.0),
p(0.0, 1.0),
]];
let eps = 0.01;
let top = Topology::from_polylines_binary(segs, EMPTY, eps);
insta::assert_ron_snapshot!(top);
}
#[test]
fn nested_squares() {
let outer = [[p(-2.0, -2.0), p(2.0, -2.0), p(2.0, 2.0), p(-2.0, 2.0)]];
let inner = [[p(-1.0, -1.0), p(1.0, -1.0), p(1.0, 1.0), p(-1.0, 1.0)]];
let eps = 0.01;
let top = Topology::from_polylines_binary(outer, inner, eps);
let contours = top.contours(|w| (w.shape_a + w.shape_b) % 2 != 0);
insta::assert_ron_snapshot!((&top, contours, top.build_scan_line_orders()));
let out_idx = top.orig_seg.indices().collect::<Vec<_>>();
let orders = top.build_scan_line_orders();
assert_eq!(orders.west_neighbor_after(out_idx[0], -2.0), None);
assert_eq!(orders.east_neighbor_after(out_idx[0], -2.2), None);
assert_eq!(
orders.east_neighbor_after(out_idx[0], -2.0),
Some(out_idx[2])
);
assert_eq!(
orders.east_neighbor_after(out_idx[0], -1.5),
Some(out_idx[2])
);
assert_eq!(
orders.east_neighbor_after(out_idx[0], -1.0),
Some(out_idx[3])
);
assert_eq!(
orders.east_neighbor_after(out_idx[0], 1.0),
Some(out_idx[2])
);
assert_eq!(
orders.east_neighbor_after(out_idx[0], 2.0),
Some(out_idx[2])
);
}
#[test]
fn squares_with_gaps() {
let mid = [[p(-2.0, -2.0), p(2.0, -2.0), p(2.0, 2.0), p(-2.0, 2.0)]];
let left_right = [
[p(-4.0, -1.0), p(-3.0, -1.0), p(-3.0, 1.0), p(-4.0, 1.0)],
[p(4.0, -1.0), p(3.0, -1.0), p(3.0, -0.5), p(4.0, -0.5)],
[p(4.0, 1.0), p(3.0, 1.0), p(3.0, 0.5), p(4.0, 0.5)],
];
let eps = 0.01;
let top = Topology::from_polylines_binary(mid, left_right, eps);
let contours = top.contours(|w| (w.shape_a + w.shape_b) % 2 != 0);
insta::assert_ron_snapshot!((&top, contours, top.build_scan_line_orders()));
}
#[test]
fn close_neighbor_height() {
let big = [[p(0.0, 0.0), p(0.0, 10.0), p(1.0, 10.0), p(1.0, 0.0)]];
let left = [
[p(-10.0, 0.5), p(-0.25, 2.0), p(-10.0, 10.0)],
[p(-10.0, 0.5), p(-0.25, 10.0), p(-10.0, 10.0)],
];
let eps = 0.5;
let top = Topology::from_polylines_binary(big, left, eps);
let indices: Vec<_> = top
.orig_seg
.indices()
.filter(|i| top.orig_seg[*i] == SegIdx(0))
.collect();
assert_eq!(indices.len(), 2);
let orders = top.build_scan_line_orders();
let mut cmp = ComparisonCache::new(eps, eps / 2.0);
let h = orders
.close_west_neighbor_height_after(
indices[0],
0.0,
&top.orig_seg,
&top.segments,
&mut cmp,
)
.unwrap();
assert!((0.5..=2.0).contains(&h));
let h = orders
.close_west_neighbor_height_after(
indices[0],
0.6,
&top.orig_seg,
&top.segments,
&mut cmp,
)
.unwrap();
assert!((0.5..=2.0).contains(&h));
let h = orders
.close_west_neighbor_height_after(
indices[1],
5.0,
&top.orig_seg,
&top.segments,
&mut cmp,
)
.unwrap();
assert!((8.0..=10.0).contains(&h));
}
#[test]
fn inner_loop() {
let outer = [[p(-2.0, -2.0), p(2.0, -2.0), p(2.0, 2.0), p(-2.0, 2.0)]];
let inners = [
[p(-1.5, -1.0), p(0.0, 2.0), p(1.5, -1.0)],
[p(-0.1, 0.0), p(0.0, 2.0), p(0.1, 0.0)],
];
let eps = 0.01;
let top = Topology::from_polylines_binary(outer, inners, eps);
let contours = top.contours(|w| (w.shape_a + w.shape_b) % 2 != 0);
insta::assert_ron_snapshot!((top, contours));
}
#[test]
fn merge_monotonic_segs() {
let mut bez = BezPath::default();
bez.move_to((-1., 0.));
bez.curve_to((-1., -1.), (1., 1.), (1., 0.));
bez.line_to((2., 4.));
bez.line_to((-2., 4.));
bez.close_path();
let top = Topology::from_path(&bez, 1e-6).unwrap();
let contours = top.contours(|w| *w != 0);
insta::assert_ron_snapshot!((top, contours));
}
#[test]
fn merge_monotonic_segs_with_cut() {
let mut bez = BezPath::default();
bez.move_to((-1., 0.));
bez.curve_to((-1., -1.), (1., 1.), (1., 0.));
bez.close_path();
let top = Topology::from_path(&bez, 1e-6).unwrap();
let contours = top.contours(|w| *w != 0);
dbg!(&top.segments.split_from_predecessor);
insta::assert_ron_snapshot!((top, contours));
}
fn run_perturbation(ps: Vec<Perturbation<F64Perturbation>>) {
let base = vec![vec![
p(0.0, 0.0),
p(1.0, 1.0),
p(1.0, -1.0),
p(2.0, 0.0),
p(1.0, 1.0),
p(1.0, -1.0),
]];
let perturbed_polylines = ps
.iter()
.map(|p| realize_perturbation(&base, p))
.collect::<Vec<_>>();
let eps = 0.1;
let _top = Topology::from_polylines_binary(perturbed_polylines, EMPTY, eps);
}
proptest! {
#[test]
fn perturbation_test_f64(perturbations in prop::collection::vec(perturbation(f64_perturbation(0.1)), 1..5)) {
run_perturbation(perturbations);
}
}
}