use std::iter::Peekable;
use kurbo::BezPath;
use crate::{
curve::AlmostHorizontalDecomposition,
geom::{Point, Segment, monotonic_pieces},
num::CheapOrderedFloat,
};
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct SegIdx(pub(crate) usize);
#[cfg_attr(test, derive(serde::Serialize))]
#[derive(Clone, Hash, PartialEq, Eq)]
#[cfg_attr(test, serde(transparent))]
pub struct SegVec<T> {
inner: Vec<T>,
}
impl_typed_vec!(SegVec, SegIdx, "s");
#[derive(Clone, Default)]
pub struct Segments {
segs: SegVec<Segment>,
horizontal_decomps: SegVec<AlmostHorizontalDecomposition>,
contour_prev: SegVec<Option<SegIdx>>,
contour_next: SegVec<Option<SegIdx>>,
orientation: SegVec<bool>,
pub(crate) split_from_predecessor: SegVec<(f64, f64)>,
pub(crate) input_segs: SegVec<kurbo::PathSeg>,
enter: Vec<(f64, SegIdx)>,
exit: Vec<(f64, SegIdx)>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NonClosedPath {
pub start_point: kurbo::Point,
pub end_point: kurbo::Point,
}
impl std::fmt::Display for NonClosedPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"non-closed path starting at {} and ending at {}",
self.start_point, self.end_point,
)
}
}
impl std::error::Error for NonClosedPath {}
struct SegmentEntryFormatter<'a> {
idx: SegIdx,
seg: &'a Segment,
prev: Option<SegIdx>,
next: Option<SegIdx>,
oriented: bool,
}
impl std::fmt::Debug for SegmentEntryFormatter<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let seg_idx = self.idx;
let seg = self.seg;
let prefix = if self.oriented {
self.prev.map(|i| format!("{i:?} -> ")).unwrap_or_default()
} else {
self.next.map(|i| format!("{i:?} <- ")).unwrap_or_default()
};
let suffix = if self.oriented {
self.next.map(|i| format!(" -> {i:?}")).unwrap_or_default()
} else {
self.prev.map(|i| format!(" <- {i:?}")).unwrap_or_default()
};
write!(f, "{seg_idx:?}: {prefix}{seg:?}{suffix}")
}
}
impl std::fmt::Debug for Segments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut list = f.debug_list();
for (idx, seg) in self.segs.iter() {
list.entry(&SegmentEntryFormatter {
idx,
seg,
prev: self.contour_prev[idx],
next: self.contour_next[idx],
oriented: self.orientation[idx],
});
}
list.finish()
}
}
fn cyclic_pairs<T>(xs: &[T]) -> impl Iterator<Item = (&T, &T)> {
pairs(xs).chain(xs.last().zip(xs.first()))
}
fn pairs<T>(xs: &[T]) -> impl Iterator<Item = (&T, &T)> {
xs.windows(2).map(|pair| (&pair[0], &pair[1]))
}
struct SubpathIter<'a, I: Iterator> {
inner: &'a mut Peekable<I>,
started: bool,
}
impl<I> Iterator for SubpathIter<'_, I>
where
I: Iterator<Item = kurbo::PathEl>,
{
type Item = kurbo::PathEl;
fn next(&mut self) -> Option<Self::Item> {
let ret = self.inner.peek()?;
if matches!(ret, kurbo::PathEl::MoveTo(_)) && self.started {
None
} else {
self.started = true;
self.inner.next()
}
}
}
struct Subpaths<I: Iterator> {
inner: Peekable<I>,
}
impl<I> Subpaths<I>
where
I: Iterator<Item = kurbo::PathEl>,
{
fn next(&mut self) -> Option<SubpathIter<'_, I>> {
if self.inner.peek().is_none() {
None
} else {
Some(SubpathIter {
inner: &mut self.inner,
started: false,
})
}
}
}
impl Segments {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.segs.len()
}
pub fn indices(&self) -> impl Iterator<Item = SegIdx> + use<> {
(0..self.segs.len()).map(SegIdx)
}
pub fn segments(&self) -> impl Iterator<Item = &Segment> {
self.segs.iter().map(|(_, s)| s)
}
pub fn oriented_start(&self, idx: SegIdx) -> Point {
if self.orientation[idx] {
self[idx].start()
} else {
self[idx].end()
}
}
pub fn oriented_end(&self, idx: SegIdx) -> Point {
if self.orientation[idx] {
self[idx].end()
} else {
self[idx].start()
}
}
pub fn contour_next(&self, idx: SegIdx) -> Option<SegIdx> {
self.contour_next[idx]
}
pub fn contour_prev(&self, idx: SegIdx) -> Option<SegIdx> {
self.contour_prev[idx]
}
pub fn positively_oriented(&self, idx: SegIdx) -> bool {
self.orientation[idx]
}
pub fn add_points<P: Into<Point>>(&mut self, ps: impl IntoIterator<Item = P>) {
let old_len = self.segs.len();
let ps: Vec<_> = ps.into_iter().map(|p| p.into()).collect();
if ps.len() <= 1 {
return;
}
for (p, q) in pairs(&ps) {
let (a, b, orient) = if p < q { (p, q, true) } else { (q, p, false) };
self.push_seg(Segment::straight(*a, *b));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs
.push(kurbo::PathSeg::Line((p.to_kurbo(), q.to_kurbo()).into()));
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
}
if old_len < self.segs.len() {
self.contour_prev[SegIdx(old_len)] = None;
*self.contour_next.inner.last_mut().unwrap() = None;
}
self.update_enter_exit(old_len);
}
pub fn add_closed_polylines<P: Into<Point>>(
&mut self,
ps: impl IntoIterator<Item = impl IntoIterator<Item = P>>,
) {
let old_len = self.segs.len();
for p in ps {
self.add_polyline_without_updating_enter_exit(p);
}
self.update_enter_exit(old_len);
}
pub fn add_closed_polyline<P: Into<Point>>(&mut self, ps: impl IntoIterator<Item = P>) {
let old_len = self.segs.len();
self.add_polyline_without_updating_enter_exit(ps);
self.update_enter_exit(old_len);
}
pub fn add_bez_path(&mut self, p: &BezPath) -> Result<(), NonClosedPath> {
let old_len = self.segs.len();
self.add_path_without_updating_enter_exit(p, true)?;
self.update_enter_exit(old_len);
Ok(())
}
pub fn add_non_closed_bez_path(&mut self, p: &BezPath) -> Result<(), NonClosedPath> {
let old_len = self.segs.len();
self.add_path_without_updating_enter_exit(p, false)?;
self.update_enter_exit(old_len);
Ok(())
}
pub(crate) fn add_path_without_updating_enter_exit(
&mut self,
p: &BezPath,
must_be_closed: bool,
) -> Result<(), NonClosedPath> {
let mut subpaths = Subpaths {
inner: p.iter().peekable(),
};
while let Some(subpath) = subpaths.next() {
let old_len = self.segs.len();
for seg in kurbo::segments(subpath) {
let pieces = monotonic_pieces(seg);
for monotonic in pieces {
self.push_seg(monotonic.piece);
self.orientation.push(monotonic.positively_oriented);
self.split_from_predecessor
.push((monotonic.start_t, monotonic.end_t));
self.input_segs.push(seg);
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
}
}
if old_len < self.segs.len() {
let start_idx = SegIdx(old_len);
let end_idx = SegIdx(self.segs.len() - 1);
let start_point = self.oriented_start(start_idx).to_kurbo();
let end_point = self.oriented_end(end_idx).to_kurbo();
if start_point == end_point {
self.contour_prev[start_idx] = Some(end_idx);
self.contour_next[end_idx] = Some(start_idx);
} else if must_be_closed {
return Err(NonClosedPath {
start_point,
end_point,
});
} else {
self.contour_prev[start_idx] = None;
self.contour_next[end_idx] = None;
}
}
}
Ok(())
}
fn add_polyline_without_updating_enter_exit<P: Into<Point>>(
&mut self,
ps: impl IntoIterator<Item = P>,
) {
let old_len = self.segs.len();
let ps: Vec<_> = ps.into_iter().map(|p| p.into()).collect();
if ps.len() <= 1 {
return;
}
for (p, q) in cyclic_pairs(&ps) {
if p == q {
continue;
}
let (a, b, orient) = if p < q { (p, q, true) } else { (q, p, false) };
self.push_seg(Segment::straight(*a, *b));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs
.push(kurbo::PathSeg::Line((p.to_kurbo(), q.to_kurbo()).into()));
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
}
if old_len < self.segs.len() {
self.contour_prev[SegIdx(old_len)] = Some(SegIdx(self.segs.len() - 1));
*self.contour_next.inner.last_mut().unwrap() = Some(SegIdx(old_len));
}
}
fn push_seg(&mut self, seg: Segment) {
let decomp = if seg.is_horizontal() {
AlmostHorizontalDecomposition::default()
} else {
AlmostHorizontalDecomposition::from_monotone_cubic(seg.to_kurbo_cubic())
};
self.horizontal_decomps.push(decomp);
self.segs.push(seg);
}
pub fn from_closed_polyline<P: Into<Point>>(ps: impl IntoIterator<Item = P>) -> Self {
let mut ret = Self::default();
ret.add_closed_polyline(ps);
ret
}
pub(crate) fn update_enter_exit(&mut self, old_len: usize) {
for idx in old_len..self.len() {
let seg_idx = SegIdx(idx);
let seg = &self.segs[seg_idx];
self.enter.push((seg.start().y, seg_idx));
if !seg.is_horizontal() {
self.exit.push((seg.end().y, seg_idx));
}
}
self.enter.sort_by(|(y1, seg1), (y2, seg2)| {
CheapOrderedFloat::from(*y1)
.cmp(&CheapOrderedFloat::from(*y2))
.then_with(|| {
CheapOrderedFloat::from(self.segs[*seg1].at_y(*y1))
.cmp(&CheapOrderedFloat::from(self.segs[*seg2].at_y(*y1)))
})
});
self.exit.sort_by(|(y1, _), (y2, _)| {
CheapOrderedFloat::from(*y1).cmp(&CheapOrderedFloat::from(*y2))
});
}
pub fn entrances(&self) -> &[(f64, SegIdx)] {
&self.enter
}
pub fn exits(&self) -> &[(f64, SegIdx)] {
&self.exit
}
pub fn check_invariants(&self) {
for (idx, seg) in self.segs.iter() {
assert!(seg.start() <= seg.end());
assert_ne!(seg.start(), seg.end());
if let Some(next_idx) = self.contour_next(idx) {
assert_eq!(self.oriented_end(idx), self.oriented_start(next_idx));
assert_eq!(self.contour_prev(next_idx), Some(idx));
}
}
}
pub(crate) fn horiz_decomp(&self, idx: SegIdx) -> &AlmostHorizontalDecomposition {
debug_assert!(!self.segs[idx].is_horizontal());
&self.horizontal_decomps[idx]
}
}
impl std::ops::Index<SegIdx> for Segments {
type Output = Segment;
fn index(&self, index: SegIdx) -> &Self::Output {
&self.segs[index]
}
}