1use ogeom_core::{OgeomResult, ogeom_bail};
33use ogeom_math::Point2;
34use spade::handles::{FixedDirectedEdgeHandle, FixedVertexHandle};
35use spade::{ConstrainedDelaunayTriangulation, Triangulation as _};
36
37#[derive(Debug, Clone, PartialEq, Default)]
42pub struct Loop2 {
43 pub points: Vec<Point2>,
45}
46
47impl Loop2 {
48 #[must_use]
51 pub fn area(&self) -> f64 {
52 let n = self.points.len();
53 let mut twice = 0.0;
54 for i in 0..n {
55 let (a, b) = (self.points[i], self.points[(i + 1) % n]);
56 twice += a.x.mul_add(b.y, -(a.y * b.x));
57 }
58 twice / 2.0
59 }
60
61 #[must_use]
63 pub fn is_hole(&self) -> bool {
64 self.area() < 0.0
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq)]
71pub struct Edge2 {
72 pub from: Point2,
74 pub to: Point2,
76 pub weight: [i32; 2],
79}
80
81pub fn region(edges: &[Edge2], inside: impl Fn([i64; 2]) -> bool) -> OgeomResult<Vec<Loop2>> {
100 if inside([0, 0]) {
101 ogeom_bail!(
102 Construction,
103 "a region accepting the unbounded face has no boundary"
104 );
105 }
106 if let Some(e) = edges
107 .iter()
108 .find(|e| !e.from.is_finite() || !e.to.is_finite())
109 {
110 ogeom_bail!(Numeric, "a chain edge from {:?} is not finite", e.from);
111 }
112 let Some(grid) = Grid::around(edges.iter().flat_map(|e| [e.from, e.to])) else {
113 return Ok(Vec::new());
114 };
115 let segments = edges
116 .iter()
117 .map(|e| Seg {
118 a: grid.snap(e.from),
119 b: grid.snap(e.to),
120 w: e.weight,
121 })
122 .collect();
123 Ok(trace(segments, &inside)?
124 .into_iter()
125 .map(|ring| Loop2 {
126 points: ring.into_iter().map(|q| grid.point(q)).collect(),
127 })
128 .collect())
129}
130
131#[allow(
134 clippy::cast_precision_loss,
135 reason = "grid coordinates are below 2^41, exact in a double"
136)]
137pub(crate) const fn real(v: i64) -> f64 {
138 v as f64
139}
140
141#[allow(clippy::cast_precision_loss, reason = "an estimate, rounding harmless")]
143pub(crate) const fn estimate(n: usize) -> f64 {
144 n as f64
145}
146
147#[allow(
149 clippy::cast_possible_truncation,
150 reason = "callers pass values rounded to integers below 2^42"
151)]
152pub(crate) const fn whole(v: f64) -> i64 {
153 v as i64
154}
155
156#[allow(
159 clippy::cast_possible_truncation,
160 clippy::cast_sign_loss,
161 reason = "indices are below 2^32"
162)]
163pub(crate) const fn slot(v: i64) -> u32 {
164 v as u32
165}
166
167pub(crate) type Q = [i64; 2];
169
170#[derive(Debug, Clone, Copy)]
173pub(crate) struct Grid {
174 origin: [f64; 2],
175 cell: f64,
176}
177
178const SPAN_BITS: i32 = 38;
180
181impl Grid {
182 pub(crate) fn around(points: impl Iterator<Item = Point2>) -> Option<Self> {
184 let mut lo = [f64::INFINITY; 2];
185 let mut hi = [f64::NEG_INFINITY; 2];
186 for p in points {
187 lo = [lo[0].min(p.x), lo[1].min(p.y)];
188 hi = [hi[0].max(p.x), hi[1].max(p.y)];
189 }
190 if lo[0] > hi[0] {
191 return None;
192 }
193 let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]);
194 let cell = if extent > 0.0 {
196 2f64.powi(i32::try_from(whole(extent.log2().ceil())).unwrap_or(0) - SPAN_BITS)
197 } else {
198 1.0
199 };
200 Some(Self {
201 origin: [(lo[0] / cell).floor() * cell, (lo[1] / cell).floor() * cell],
202 cell,
203 })
204 }
205
206 pub(crate) fn snap(&self, p: Point2) -> Q {
208 let k = |v: f64, o: f64| 2 * whole(((v - o) / self.cell).round());
209 [k(p.x, self.origin[0]), k(p.y, self.origin[1])]
210 }
211
212 pub(crate) fn point(&self, q: Q) -> Point2 {
214 let c = self.cell / 2.0;
215 Point2::new(
216 real(q[0]).mul_add(c, self.origin[0]),
217 real(q[1]).mul_add(c, self.origin[1]),
218 )
219 }
220}
221
222#[derive(Debug, Clone, Copy)]
224pub(crate) struct Seg {
225 pub(crate) a: Q,
226 pub(crate) b: Q,
227 pub(crate) w: [i32; 2],
228}
229
230pub(crate) fn orient(a: Q, b: Q, c: Q) -> i128 {
233 let d = |p: Q, q: Q| [i128::from(q[0] - p[0]), i128::from(q[1] - p[1])];
234 let (u, v) = (d(a, b), d(a, c));
235 u[0] * v[1] - u[1] * v[0]
236}
237
238fn cancel(mut segments: Vec<Seg>) -> Vec<Seg> {
241 for s in &mut segments {
242 if s.b < s.a {
243 *s = Seg {
244 a: s.b,
245 b: s.a,
246 w: [-s.w[0], -s.w[1]],
247 };
248 }
249 }
250 segments.sort_unstable_by_key(|s| (s.a, s.b));
251 let mut out: Vec<Seg> = Vec::with_capacity(segments.len());
252 for s in segments {
253 match out.last_mut() {
254 Some(t) if t.a == s.a && t.b == s.b => {
255 t.w = [t.w[0] + s.w[0], t.w[1] + s.w[1]];
256 }
257 _ => out.push(s),
258 }
259 }
260 out.retain(|s| s.a != s.b && s.w != [0, 0]);
261 out
262}
263
264struct Buckets {
266 lo: Q,
267 size: i64,
268 columns: i64,
269 rows: i64,
270}
271
272impl Buckets {
273 fn over(segments: &[Seg]) -> Self {
274 let mut lo = [i64::MAX; 2];
275 let mut hi = [i64::MIN; 2];
276 for s in segments {
277 for p in [s.a, s.b] {
278 lo = [lo[0].min(p[0]), lo[1].min(p[1])];
279 hi = [hi[0].max(p[0]), hi[1].max(p[1])];
280 }
281 }
282 lo = [lo[0] - 2, lo[1] - 2];
284 hi = [hi[0] + 2, hi[1] + 2];
285 let span = (hi[0] - lo[0]).max(hi[1] - lo[1]) + 1;
286 let per_side = whole(estimate(segments.len()).sqrt().ceil()).clamp(1, 4096);
287 let size = (span + per_side - 1) / per_side;
288 Self {
289 lo,
290 size,
291 columns: (hi[0] - lo[0]) / size + 1,
292 rows: (hi[1] - lo[1]) / size + 1,
293 }
294 }
295
296 fn column(&self, x: i64) -> i64 {
297 ((x - self.lo[0]) / self.size).clamp(0, self.columns - 1)
298 }
299
300 fn row(&self, y: i64) -> i64 {
301 ((y - self.lo[1]) / self.size).clamp(0, self.rows - 1)
302 }
303
304 fn of(&self, p: Q) -> u32 {
305 slot(self.row(p[1]) * self.columns + self.column(p[0]))
306 }
307
308 fn along(&self, s: &Seg, mut visit: impl FnMut(u32)) {
313 let (y_lo, y_hi) = (s.a[1].min(s.b[1]) - 2, s.a[1].max(s.b[1]) + 2);
314 let (x_lo, x_hi) = (s.a[0].min(s.b[0]) - 2, s.a[0].max(s.b[0]) + 2);
315 let dx = real(s.b[0] - s.a[0]);
316 let dy = real(s.b[1] - s.a[1]);
317 let x_at = |y: i64| (real(y - s.a[1]) * dx / dy) + real(s.a[0]);
318 for r in self.row(y_lo)..=self.row(y_hi) {
319 let (from, to) = if dy == 0.0 {
320 (x_lo, x_hi)
321 } else {
322 let top = self.lo[1] + r * self.size;
323 let y0 = top.max(y_lo);
324 let y1 = (top + self.size - 1).min(y_hi);
325 let (xa, xb) = (x_at(y0), x_at(y1));
326 (
327 whole((xa.min(xb) - 2.0).floor()).max(x_lo),
328 whole((xa.max(xb) + 2.0).ceil()).min(x_hi),
329 )
330 };
331 for c in self.column(from)..=self.column(to) {
332 visit(slot(r * self.columns + c));
333 }
334 }
335 }
336
337 fn lists(&self, pairs: &mut [(u32, u32)]) -> (Vec<usize>, Vec<u32>) {
339 pairs.sort_unstable();
340 let count = slot(self.rows * self.columns) as usize;
341 let mut start = vec![0usize; count + 1];
342 for &(b, _) in pairs.iter() {
343 start[b as usize + 1] += 1;
344 }
345 for k in 0..count {
346 start[k + 1] += start[k];
347 }
348 (start, pairs.iter().map(|&(_, i)| i).collect())
349 }
350}
351
352fn crossing(s: &Seg, t: &Seg) -> Option<Q> {
356 let s1 = orient(s.a, s.b, t.a).signum();
357 let s2 = orient(s.a, s.b, t.b).signum();
358 if s1 * s2 >= 0 {
359 return None;
360 }
361 let along = orient(t.a, t.b, s.a);
362 let other = orient(t.a, t.b, s.b);
363 if along.signum() * other.signum() >= 0 {
364 return None;
365 }
366 let (mut num, mut den) = (along, along - other);
369 if den < 0 {
370 (num, den) = (-num, -den);
371 }
372 let cell = |a: i64, b: i64| {
373 let x = i128::from(a) * den + i128::from(b - a) * num;
374 i64::try_from(2 * (x + den).div_euclid(2 * den)).unwrap_or(i64::MAX)
375 };
376 Some([cell(s.a[0], s.b[0]), cell(s.a[1], s.b[1])])
377}
378
379#[derive(Debug, Clone, Copy)]
382struct Bound {
383 num: i128,
384 den: i128,
385 open: bool,
386}
387
388impl Bound {
389 fn cmp(self, other: Self) -> core::cmp::Ordering {
391 self.value_cmp(other).then(self.open.cmp(&other.open))
392 }
393
394 fn value_cmp(self, other: Self) -> core::cmp::Ordering {
397 (self.num * other.den).cmp(&(other.num * self.den))
398 }
399}
400
401fn entry(s: &Seg, c: Q) -> Option<Bound> {
404 let mut lo = Bound {
405 num: 0,
406 den: 1,
407 open: false,
408 };
409 let mut hi = Bound {
410 num: 1,
411 den: 1,
412 open: false,
413 };
414 for (k, ¢re) in c.iter().enumerate() {
415 let (p, d) = (i128::from(s.a[k]), i128::from(s.b[k] - s.a[k]));
416 let (left, right) = (i128::from(centre - 1), i128::from(centre + 1));
417 if d == 0 {
418 if p < left || p >= right {
419 return None;
420 }
421 continue;
422 }
423 let (from, to) = if d > 0 {
425 (
426 Bound {
427 num: left - p,
428 den: d,
429 open: false,
430 },
431 Bound {
432 num: right - p,
433 den: d,
434 open: true,
435 },
436 )
437 } else {
438 (
439 Bound {
440 num: p - right,
441 den: -d,
442 open: true,
443 },
444 Bound {
445 num: p - left,
446 den: -d,
447 open: false,
448 },
449 )
450 };
451 if from.cmp(lo).is_gt() {
453 lo = from;
454 }
455 let upper = to.value_cmp(hi);
456 if upper.is_lt() || (upper.is_eq() && to.open) {
457 hi = to;
458 }
459 }
460 let ordered = lo.value_cmp(hi);
461 let meets = ordered.is_lt() || (ordered.is_eq() && !lo.open && !hi.open);
462 meets.then_some(lo)
463}
464
465pub(crate) fn trace(
468 segments: Vec<Seg>,
469 inside: &dyn Fn([i64; 2]) -> bool,
470) -> OgeomResult<Vec<Vec<Q>>> {
471 let segments = cancel(segments);
472 if segments.is_empty() {
473 return Ok(Vec::new());
474 }
475 let buckets = Buckets::over(&segments);
476 let mut pairs = Vec::with_capacity(segments.len() * 2);
477 for (i, s) in segments.iter().enumerate() {
478 buckets.along(s, |b| pairs.push((b, slot(i as i64))));
479 }
480 let (start, held) = buckets.lists(&mut pairs);
481
482 let mut hot: Vec<Q> = segments.iter().flat_map(|s| [s.a, s.b]).collect();
485 for b in 0..start.len() - 1 {
486 let here = &held[start[b]..start[b + 1]];
487 for (k, &i) in here.iter().enumerate() {
488 let s = &segments[i as usize];
489 for &j in &here[k + 1..] {
490 let t = &segments[j as usize];
491 if let Some(c) = crossing(s, t)
492 && buckets.of(c) as usize == b
493 {
494 hot.push(c);
495 }
496 }
497 }
498 }
499 hot.sort_unstable();
500 hot.dedup();
501 let mut placed: Vec<(u32, u32)> = hot
502 .iter()
503 .enumerate()
504 .map(|(k, &c)| (buckets.of(c), slot(k as i64)))
505 .collect();
506 let (hot_start, hot_held) = buckets.lists(&mut placed);
507
508 let mut rounded = Vec::with_capacity(segments.len());
510 let mut met: Vec<(Bound, Q)> = Vec::new();
511 for s in &segments {
512 met.clear();
513 buckets.along(s, |b| {
514 let b = b as usize;
515 for &k in &hot_held[hot_start[b]..hot_start[b + 1]] {
516 let c = hot[k as usize];
517 if let Some(at) = entry(s, c) {
518 met.push((at, c));
519 }
520 }
521 });
522 met.sort_unstable_by(|x, y| x.0.cmp(y.0));
523 for pair in met.windows(2) {
524 rounded.push(Seg {
525 a: pair[0].1,
526 b: pair[1].1,
527 w: s.w,
528 });
529 }
530 }
531 regions_of(&cancel(rounded), inside)
532}
533
534fn regions_of(segments: &[Seg], inside: &dyn Fn([i64; 2]) -> bool) -> OgeomResult<Vec<Vec<Q>>> {
537 if segments.is_empty() {
538 return Ok(Vec::new());
539 }
540 let mut points: Vec<Q> = segments.iter().flat_map(|s| [s.a, s.b]).collect();
541 points.sort_unstable();
542 points.dedup();
543 let index = |q: Q| points.binary_search(&q).unwrap_or(0);
544 let vertices: Vec<spade::Point2<f64>> = points
545 .iter()
546 .map(|q| spade::Point2::new(real(q[0]), real(q[1])))
547 .collect();
548 let Ok(mut cdt) =
549 ConstrainedDelaunayTriangulation::<spade::Point2<f64>>::bulk_load_cdt(vertices, Vec::new())
550 else {
551 ogeom_bail!(Numeric, "the region's points could not be triangulated");
552 };
553 let mut constraints: Vec<(FixedDirectedEdgeHandle, [i32; 2])> = Vec::new();
554 for s in segments {
555 let (from, to) = (index(s.a), index(s.b));
556 let made = cdt.try_add_constraint(
557 FixedVertexHandle::from_index(from),
558 FixedVertexHandle::from_index(to),
559 );
560 if made.is_empty() {
561 ogeom_bail!(
562 Construction,
563 "a snap-rounded segment crosses another; the region is not traced"
564 );
565 }
566 let along = [s.b[0] - s.a[0], s.b[1] - s.a[1]];
567 for h in made {
568 let e = cdt.directed_edge(h);
569 let [p, q] = e.positions();
570 let forward = (q.x - p.x).mul_add(real(along[0]), (q.y - p.y) * real(along[1])) > 0.0;
571 constraints.push((if forward { h } else { h.rev() }, s.w));
572 }
573 }
574 let mut weight = vec![[0i64; 2]; cdt.num_directed_edges()];
576 for (h, w) in constraints {
577 for k in 0..2 {
578 weight[h.index()][k] += i64::from(w[k]);
579 weight[h.rev().index()][k] -= i64::from(w[k]);
580 }
581 }
582
583 let mut count: Vec<Option<[i64; 2]>> = vec![None; cdt.num_all_faces()];
586 let outer = cdt.outer_face().fix().index();
587 count[outer] = Some([0, 0]);
588 let mut sides = vec![[None; 3]; cdt.num_all_faces()];
589 for face in cdt.inner_faces() {
590 sides[face.fix().index()] = face.adjacent_edges().map(|e| Some(e.fix()));
591 }
592 let mut stack = vec![outer];
593 let mut around = Vec::with_capacity(3);
594 while let Some(f) = stack.pop() {
595 let Some(here) = count[f] else { continue };
596 around.clear();
597 if f == outer {
598 for e in cdt.convex_hull() {
599 around.push(if e.face().is_outer() {
600 e.fix()
601 } else {
602 e.rev().fix()
603 });
604 }
605 } else {
606 around.extend(sides[f].iter().flatten());
607 }
608 for &e in &around {
609 let w = weight[e.index()];
610 let g = cdt.directed_edge(e).rev().face().fix().index();
611 if count[g].is_none() {
612 count[g] = Some([here[0] - w[0], here[1] - w[1]]);
613 stack.push(g);
614 }
615 }
616 }
617 let accepted: Vec<bool> = count
618 .iter()
619 .enumerate()
620 .map(|(f, c)| f != outer && c.is_some_and(inside))
621 .collect();
622
623 let boundary = |e: FixedDirectedEdgeHandle| {
624 let h = cdt.directed_edge(e);
625 accepted[h.face().fix().index()] && !accepted[h.rev().face().fix().index()]
626 };
627 let mut used = vec![false; cdt.num_directed_edges()];
628 let mut loops = Vec::new();
629 for e in cdt.directed_edges() {
630 let first = e.fix();
631 if used[first.index()] || !boundary(first) {
632 continue;
633 }
634 let mut ring = Vec::new();
635 let mut at = first;
636 loop {
637 used[at.index()] = true;
638 let h = cdt.directed_edge(at);
639 ring.push(points[h.from().fix().index()]);
640 let mut next = h.next();
643 let mut turns = 0usize;
644 while !boundary(next.fix()) {
645 next = next.rev().next();
646 turns += 1;
647 if turns > cdt.num_directed_edges() {
648 ogeom_bail!(Construction, "a region boundary does not link up");
649 }
650 }
651 at = next.fix();
652 if at == first {
653 break;
654 }
655 if used[at.index()] {
656 ogeom_bail!(Construction, "a region boundary links into itself");
657 }
658 }
659 loops.push(straightened(ring));
660 }
661 Ok(loops)
662}
663
664fn straightened(mut ring: Vec<Q>) -> Vec<Q> {
666 loop {
667 let n = ring.len();
668 if n < 3 {
669 return ring;
670 }
671 let keep: Vec<bool> = (0..n)
672 .map(|i| orient(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]) != 0)
673 .collect();
674 if keep.iter().all(|&k| k) {
675 return ring;
676 }
677 let mut k = 0;
678 ring.retain(|_| {
679 k += 1;
680 keep[k - 1]
681 });
682 }
683}
684
685#[cfg(test)]
686mod tests {
687 #![allow(clippy::unwrap_used, reason = "test code")]
688 use super::*;
689
690 fn square(x: f64, y: f64, s: f64) -> Vec<Edge2> {
691 let p = [
692 Point2::new(x, y),
693 Point2::new(x + s, y),
694 Point2::new(x + s, y + s),
695 Point2::new(x, y + s),
696 ];
697 (0..4)
698 .map(|i| Edge2 {
699 from: p[i],
700 to: p[(i + 1) % 4],
701 weight: [1, 0],
702 })
703 .collect()
704 }
705
706 #[test]
710 fn two_overlapping_squares_unite() {
711 let mut edges = square(0.0, 0.0, 1.0);
712 edges.extend(square(0.5, 0.5, 1.0));
713 let loops = region(&edges, |w| w[0] > 0).unwrap();
714 assert_eq!(loops.len(), 1);
715 assert_eq!(loops[0].points.len(), 8);
716 assert!((loops[0].area() - 1.75).abs() < 1e-9);
717 let both = region(&edges, |w| w[0] > 1).unwrap();
718 assert_eq!(both.len(), 1);
719 assert!((both[0].area() - 0.25).abs() < 1e-9);
720 }
721
722 #[test]
725 fn a_difference_keeps_its_hole() {
726 let mut edges = square(0.0, 0.0, 3.0);
727 edges.extend(square(1.0, 1.0, 1.0).into_iter().map(|e| Edge2 {
728 weight: [0, 1],
729 ..e
730 }));
731 let loops = region(&edges, |w| w[0] > 0 && w[1] == 0).unwrap();
732 assert_eq!(loops.len(), 2);
733 let mut areas: Vec<f64> = loops.iter().map(Loop2::area).collect();
734 areas.sort_by(f64::total_cmp);
735 assert!((areas[0] + 1.0).abs() < 1e-9 && (areas[1] - 9.0).abs() < 1e-9);
736 }
737
738 #[test]
740 fn squares_touching_at_a_corner_stay_apart() {
741 let mut edges = square(0.0, 0.0, 1.0);
742 edges.extend(square(1.0, 1.0, 1.0));
743 let loops = region(&edges, |w| w[0] > 0).unwrap();
744 assert_eq!(loops.len(), 2);
745 assert!(loops.iter().all(|l| l.points.len() == 4));
746 }
747
748 #[test]
753 fn a_fan_of_slivers_crossing_near_a_point() {
754 let mut edges = Vec::new();
755 for k in 0..64 {
756 let a = f64::from(k) * 0.049_087_385;
757 let (c, s) = (a.cos(), a.sin());
758 let p = [
759 Point2::new(-c + 1e-9 * s, -s - 1e-9 * c),
760 Point2::new(c + 1e-9 * s, s - 1e-9 * c),
761 Point2::new(c - 1e-3 * s, s + 1e-3 * c),
762 ];
763 for i in 0..3 {
764 edges.push(Edge2 {
765 from: p[i],
766 to: p[(i + 1) % 3],
767 weight: [1, 0],
768 });
769 }
770 }
771 let loops = region(&edges, |w| w[0] > 0).unwrap();
772 let area: f64 = loops.iter().map(Loop2::area).sum();
773 assert!(area > 0.0 && area < 64.0 * 1e-3);
774 }
775}