1use super::boolean_2::point_in_polygon;
49use super::primitives::Point2;
50
51#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct Line2 {
59 pub slope: f64,
61 pub intercept: f64,
63 pub is_vertical: bool,
65 pub x_const: f64,
67}
68
69impl Line2 {
70 pub fn new(slope: f64, intercept: f64) -> Self {
72 Self {
73 slope,
74 intercept,
75 is_vertical: false,
76 x_const: 0.0,
77 }
78 }
79
80 pub fn vertical(x_const: f64) -> Self {
82 Self {
83 slope: 0.0,
84 intercept: 0.0,
85 is_vertical: true,
86 x_const,
87 }
88 }
89
90 pub fn through_points(a: Point2, b: Point2) -> Self {
92 let dx = b.x - a.x;
93 if dx.abs() <= f64::MIN_POSITIVE {
94 Self::vertical(a.x)
95 } else {
96 let slope = (b.y - a.y) / dx;
97 let intercept = a.y - slope * a.x;
98 Self::new(slope, intercept)
99 }
100 }
101
102 #[inline]
105 pub fn y_at(&self, x: f64) -> f64 {
106 if self.is_vertical {
107 self.x_const
108 } else {
109 self.slope * x + self.intercept
110 }
111 }
112
113 #[inline]
116 pub fn x_at(&self, y: f64) -> f64 {
117 if self.is_vertical {
118 self.x_const
119 } else if self.slope.abs() <= f64::MIN_POSITIVE {
120 f64::NAN
122 } else {
123 (y - self.intercept) / self.slope
124 }
125 }
126
127 #[inline]
129 pub fn is_parallel(&self, other: &Line2) -> bool {
130 if self.is_vertical && other.is_vertical {
131 return true;
132 }
133 if self.is_vertical || other.is_vertical {
134 return false;
135 }
136 (self.slope - other.slope).abs()
137 <= f64::EPSILON * (self.slope.abs() + other.slope.abs() + 1.0)
138 }
139}
140
141pub fn line_line_intersection(l1: &Line2, l2: &Line2) -> Option<Point2> {
143 if l1.is_vertical && l2.is_vertical {
144 return None; }
146 if l1.is_vertical {
147 return Some(Point2::new(l1.x_const, l2.y_at(l1.x_const)));
148 }
149 if l2.is_vertical {
150 return Some(Point2::new(l2.x_const, l1.y_at(l2.x_const)));
151 }
152 if l1.is_parallel(l2) {
153 return None;
154 }
155 let x = (l2.intercept - l1.intercept) / (l1.slope - l2.slope);
156 let y = l1.y_at(x);
157 Some(Point2::new(x, y))
158}
159
160#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct ArrangementEdge {
169 pub line: usize,
171 pub start: Point2,
173 pub end: Point2,
175}
176
177#[derive(Debug, Clone, PartialEq)]
180pub struct ArrangementFace {
181 pub boundary: Vec<Point2>,
183 pub unbounded: bool,
186}
187
188#[derive(Debug, Clone, PartialEq)]
190pub struct Arrangement {
191 pub lines: Vec<Line2>,
193 pub vertices: Vec<Point2>,
195 pub edges: Vec<ArrangementEdge>,
197 pub faces: Vec<ArrangementFace>,
199 pub bbox_min: Point2,
201 pub bbox_max: Point2,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct ArrangementCounts {
207 pub vertices: usize,
208 pub edges: usize,
209 pub faces: usize,
210 pub euler: i64,
212}
213
214impl Arrangement {
215 pub fn counts(&self) -> ArrangementCounts {
216 let v = self.vertices.len() as i64;
217 let e = self.edges.len() as i64;
218 let f = self.faces.len() as i64;
219 ArrangementCounts {
220 vertices: self.vertices.len(),
221 edges: self.edges.len(),
222 faces: self.faces.len(),
223 euler: v - e + f,
224 }
225 }
226}
227
228#[derive(Debug, Clone, PartialEq)]
229pub enum ArrangementError {
230 TooFewLines { got: usize },
231 AllParallel,
232}
233
234const BBOX_MARGIN: f64 = 3.0;
241
242fn compute_bbox(vertices: &[Point2]) -> (Point2, Point2) {
244 if vertices.is_empty() {
245 return (Point2::new(-10.0, -10.0), Point2::new(10.0, 10.0));
246 }
247 let mut min_x = f64::INFINITY;
248 let mut max_x = f64::NEG_INFINITY;
249 let mut min_y = f64::INFINITY;
250 let mut max_y = f64::NEG_INFINITY;
251 for &v in vertices {
252 min_x = min_x.min(v.x);
253 max_x = max_x.max(v.x);
254 min_y = min_y.min(v.y);
255 max_y = max_y.max(v.y);
256 }
257 let dx = (max_x - min_x).max(1.0);
258 let dy = (max_y - min_y).max(1.0);
259 (
260 Point2::new(min_x - BBOX_MARGIN * dx, min_y - BBOX_MARGIN * dy),
261 Point2::new(max_x + BBOX_MARGIN * dx, max_y + BBOX_MARGIN * dy),
262 )
263}
264
265fn clip_line_to_bbox(line: &Line2, bmin: Point2, bmax: Point2) -> (Point2, Point2) {
268 let (p0, d) = if line.is_vertical {
271 (
272 Point2::new(line.x_const, bmin.y),
273 Point2::new(0.0, bmax.y - bmin.y),
274 )
275 } else {
276 let x0 = bmin.x - (bmax.x - bmin.x);
278 let x1 = bmax.x + (bmax.x - bmin.x);
279 (
280 Point2::new(x0, line.y_at(x0)),
281 Point2::new(x1 - x0, line.y_at(x1) - line.y_at(x0)),
282 )
283 };
284
285 let (t0, t1) = liang_barsky(p0, d, bmin, bmax);
286 (
287 Point2::new(p0.x + t0 * d.x, p0.y + t0 * d.y),
288 Point2::new(p0.x + t1 * d.x, p0.y + t1 * d.y),
289 )
290}
291
292fn liang_barsky(p0: Point2, d: Point2, bmin: Point2, bmax: Point2) -> (f64, f64) {
295 let mut t0 = 0.0f64;
296 let mut t1 = 1.0f64;
297
298 for (p, q) in [
299 (-d.x, p0.x - bmin.x), (d.x, bmax.x - p0.x), (-d.y, p0.y - bmin.y), (d.y, bmax.y - p0.y), ] {
304 if p.abs() <= f64::MIN_POSITIVE {
305 if q < 0.0 {
307 return (0.0, 0.0); }
309 } else {
310 let r = q / p;
311 if p < 0.0 {
312 t0 = t0.max(r);
313 } else {
314 t1 = t1.min(r);
315 }
316 }
317 }
318 (t0, t1)
319}
320
321pub fn build_line_arrangement(lines: &[Line2]) -> Result<Arrangement, ArrangementError> {
327 let n = lines.len();
328 if n < 2 {
329 return Err(ArrangementError::TooFewLines { got: n });
330 }
331
332 let mut vertices: Vec<Point2> = Vec::new();
334 for i in 0..n {
335 for j in (i + 1)..n {
336 if let Some(p) = line_line_intersection(&lines[i], &lines[j]) {
337 if p.x.is_finite() && p.y.is_finite() {
338 vertices.push(p);
339 }
340 }
341 }
342 }
343
344 if vertices.is_empty() {
345 return Err(ArrangementError::AllParallel);
346 }
347
348 vertices.sort_by(|a, b| {
350 a.x.partial_cmp(&b.x)
351 .unwrap_or(std::cmp::Ordering::Equal)
352 .then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
353 });
354 vertices.dedup_by(|a, b| (a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() < 1e-9);
355
356 let (bmin, bmax) = compute_bbox(&vertices);
358
359 let mut edges: Vec<ArrangementEdge> = Vec::new();
362 let mut boundary_points: Vec<Point2> = Vec::new();
364 for (li, line) in lines.iter().enumerate() {
365 let (clip_a, clip_b) = clip_line_to_bbox(line, bmin, bmax);
367
368 boundary_points.push(clip_a);
370 boundary_points.push(clip_b);
371
372 let mut pts: Vec<(f64, Point2)> = Vec::new();
374 pts.push((0.0, clip_a));
375 pts.push((1.0, clip_b));
376 for &v in &vertices {
377 if point_on_line(v, line) {
378 let dx = clip_b.x - clip_a.x;
380 let dy = clip_b.y - clip_a.y;
381 let len_sq = dx * dx + dy * dy;
382 if len_sq > f64::MIN_POSITIVE {
383 let t = ((v.x - clip_a.x) * dx + (v.y - clip_a.y) * dy) / len_sq;
384 if t > 1e-9 && t < 1.0 - 1e-9 {
385 pts.push((t, v));
386 }
387 }
388 }
389 }
390 pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
391 pts.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-12);
392
393 for w in pts.windows(2) {
395 let start = w[0].1;
396 let end = w[1].1;
397 if (start.x - end.x).abs() > 1e-12 || (start.y - end.y).abs() > 1e-12 {
398 edges.push(ArrangementEdge {
399 line: li,
400 start,
401 end,
402 });
403 }
404 }
405 }
406
407 let bbox_edges = build_bbox_boundary_edges(bmin, bmax, &boundary_points);
410 edges.extend(bbox_edges);
411
412 let mut all_vertices: Vec<Point2> = vertices.clone();
417 for e in &edges {
418 insert_point(&mut all_vertices, e.start);
419 insert_point(&mut all_vertices, e.end);
420 }
421 all_vertices.sort_by(|a, b| {
423 a.x.partial_cmp(&b.x)
424 .unwrap_or(std::cmp::Ordering::Equal)
425 .then(a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
426 });
427
428 let faces = extract_faces(&edges, &all_vertices, bmin, bmax);
435
436 Ok(Arrangement {
437 lines: lines.to_vec(),
438 vertices: all_vertices,
439 edges,
440 faces,
441 bbox_min: bmin,
442 bbox_max: bmax,
443 })
444}
445
446fn point_on_line(p: Point2, line: &Line2) -> bool {
448 if line.is_vertical {
449 return (p.x - line.x_const).abs() < 1e-9;
450 }
451 let y_expected = line.y_at(p.x);
452 (p.y - y_expected).abs() < 1e-9 * (y_expected.abs() + 1.0)
453}
454
455fn build_bbox_boundary_edges(
461 bmin: Point2,
462 bmax: Point2,
463 boundary_points: &[Point2],
464) -> Vec<ArrangementEdge> {
465 let corners = [
466 Point2::new(bmin.x, bmin.y), Point2::new(bmax.x, bmin.y), Point2::new(bmax.x, bmax.y), Point2::new(bmin.x, bmax.y), ];
471
472 let sides: [(Point2, Point2, bool); 4] = [
478 (corners[0], corners[1], true), (corners[1], corners[2], false), (corners[2], corners[3], true), (corners[3], corners[0], false), ];
483
484 let mut edges = Vec::new();
485 for (start, end, horizontal) in sides {
486 let mut pts: Vec<(f64, Point2)> = Vec::new();
488 pts.push((0.0, start));
489 pts.push((1.0, end));
490 for &bp in boundary_points {
491 if horizontal {
492 if (bp.y - start.y).abs() < 1e-9
494 && bp.x >= start.x.min(end.x) - 1e-9
495 && bp.x <= start.x.max(end.x) + 1e-9
496 {
497 let t = (bp.x - start.x) / (end.x - start.x);
498 if t > 1e-9 && t < 1.0 - 1e-9 {
499 pts.push((t, bp));
500 }
501 }
502 } else {
503 if (bp.x - start.x).abs() < 1e-9
505 && bp.y >= start.y.min(end.y) - 1e-9
506 && bp.y <= start.y.max(end.y) + 1e-9
507 {
508 let t = (bp.y - start.y) / (end.y - start.y);
509 if t > 1e-9 && t < 1.0 - 1e-9 {
510 pts.push((t, bp));
511 }
512 }
513 }
514 }
515 pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
516 pts.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-12);
517
518 for w in pts.windows(2) {
519 let s = w[0].1;
520 let e = w[1].1;
521 if (s.x - e.x).abs() > 1e-12 || (s.y - e.y).abs() > 1e-12 {
522 edges.push(ArrangementEdge {
523 line: usize::MAX, start: s,
525 end: e,
526 });
527 }
528 }
529 }
530 edges
531}
532
533#[derive(Clone, Copy)]
539struct ArrHalfEdge {
540 origin: usize, twin: usize, next: usize, face: usize, }
545
546fn extract_faces(
550 edges: &[ArrangementEdge],
551 vertices: &[Point2],
552 _bmin: Point2,
553 _bmax: Point2,
554) -> Vec<ArrangementFace> {
555 let mut all_points: Vec<Point2> = vertices.to_vec();
558 for e in edges {
559 insert_point(&mut all_points, e.start);
560 insert_point(&mut all_points, e.end);
561 }
562
563 let n_pts = all_points.len();
564 let m = edges.len();
565
566 let mut he = vec![
568 ArrHalfEdge {
569 origin: 0,
570 twin: 0,
571 next: 0,
572 face: usize::MAX,
573 };
574 2 * m
575 ];
576
577 for (i, e) in edges.iter().enumerate() {
578 let from = find_point(&all_points, e.start).unwrap();
579 let to = find_point(&all_points, e.end).unwrap();
580 let f = i;
581 let t = i + m;
582 he[f].origin = from;
583 he[f].twin = t;
584 he[t].origin = to;
585 he[t].twin = f;
586 }
587
588 let mut outgoing: Vec<Vec<usize>> = vec![Vec::new(); n_pts];
590 for (i, h) in he.iter().enumerate() {
591 outgoing[h.origin].push(i);
592 }
593 for v in 0..n_pts {
594 let o = all_points[v];
595 outgoing[v].sort_by(|&h1, &h2| {
596 let d1 = all_points[he[he[h1].twin].origin];
597 let d2 = all_points[he[he[h2].twin].origin];
598 let a1 = (d1.y - o.y).atan2(d1.x - o.x);
599 let a2 = (d2.y - o.y).atan2(d2.x - o.x);
600 a1.partial_cmp(&a2).unwrap_or(std::cmp::Ordering::Equal)
601 });
602 }
603
604 for v in 0..n_pts {
606 let gs = &outgoing[v];
607 let k = gs.len();
608 if k == 0 {
609 continue;
610 }
611 for i in 0..k {
612 let g_i = gs[i];
613 let twin = he[g_i].twin;
614 let next = gs[(i + k - 1) % k];
615 he[twin].next = next;
616 }
617 }
618
619 let n_he = he.len();
621 let mut face_id = 0usize;
622 let mut face_cycles: Vec<Vec<usize>> = Vec::new();
623 let mut face_start: Vec<usize> = Vec::new();
624 for start in 0..n_he {
625 if he[start].face != usize::MAX {
626 continue;
627 }
628 let mut cycle: Vec<usize> = Vec::new();
629 let mut cur = start;
630 loop {
631 if he[cur].face != usize::MAX {
632 break;
633 }
634 he[cur].face = face_id;
635 cycle.push(cur);
636 cur = he[cur].next;
637 if cur == start {
638 break;
639 }
640 if cycle.len() > n_he {
641 break;
642 }
643 }
644 face_cycles.push(cycle);
645 face_start.push(start);
646 face_id += 1;
647 }
648
649 let mut faces: Vec<ArrangementFace> = Vec::with_capacity(face_id);
651 for cyc in &face_cycles {
652 let boundary: Vec<Point2> = cyc.iter().map(|&h| all_points[he[h].origin]).collect();
653 let area = signed_area(&boundary);
654 let unbounded = area < 0.0;
657 faces.push(ArrangementFace {
658 boundary,
659 unbounded,
660 });
661 }
662
663 for f in &mut faces {
665 if f.unbounded {
666 f.boundary.reverse();
667 }
668 }
669
670 faces
671}
672
673fn insert_point(points: &mut Vec<Point2>, p: Point2) {
675 if find_point(points, p).is_some() {
676 return;
677 }
678 points.push(p);
679}
680
681fn find_point(points: &[Point2], p: Point2) -> Option<usize> {
683 for (i, q) in points.iter().enumerate() {
684 if (p.x - q.x).abs() < 1e-9 && (p.y - q.y).abs() < 1e-9 {
685 return Some(i);
686 }
687 }
688 None
689}
690
691fn signed_area(vertices: &[Point2]) -> f64 {
693 let n = vertices.len();
694 if n < 3 {
695 return 0.0;
696 }
697 let mut area = 0.0;
698 for i in 0..n {
699 let j = (i + 1) % n;
700 area += vertices[i].x * vertices[j].y - vertices[j].x * vertices[i].y;
701 }
702 area * 0.5
703}
704
705pub fn zone_traversal(arr: &Arrangement, query: &Line2) -> Vec<usize> {
712 let mut crossings: Vec<(f64, usize)> = Vec::new(); let (q_start, q_dir) = if query.is_vertical {
717 (
718 Point2::new(query.x_const, arr.bbox_min.y - 1.0),
719 Point2::new(0.0, arr.bbox_max.y - arr.bbox_min.y + 2.0),
720 )
721 } else {
722 let x0 = arr.bbox_min.x - 1.0;
723 let x1 = arr.bbox_max.x + 1.0;
724 (
725 Point2::new(x0, query.y_at(x0)),
726 Point2::new(x1 - x0, query.y_at(x1) - query.y_at(x0)),
727 )
728 };
729
730 for (ei, e) in arr.edges.iter().enumerate() {
731 if let Some(t) = segment_segment_parametric(q_start, q_dir, e.start, e.end) {
733 if t > 1e-9 && t < 1.0 - 1e-9 {
734 crossings.push((t, ei));
735 }
736 }
737 }
738
739 crossings.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
741 crossings.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-12);
742
743 if crossings.is_empty() {
744 let mid = Point2::new(q_start.x + 0.5 * q_dir.x, q_start.y + 0.5 * q_dir.y);
747 if let Some(fi) = locate_face(arr, mid) {
748 return vec![fi];
749 }
750 return Vec::new();
751 }
752
753 let mut zone: Vec<usize> = Vec::new();
756 for w in crossings.windows(2) {
757 let t_mid = (w[0].0 + w[1].0) * 0.5;
758 let mid = Point2::new(q_start.x + t_mid * q_dir.x, q_start.y + t_mid * q_dir.y);
759 if let Some(fi) = locate_face(arr, mid) {
760 zone.push(fi);
761 }
762 }
763
764 zone
765}
766
767fn segment_segment_parametric(p0: Point2, d: Point2, a: Point2, b: Point2) -> Option<f64> {
770 let s = Point2::new(b.x - a.x, b.y - a.y);
771 let denom = d.x * s.y - d.y * s.x;
772 if denom.abs() <= f64::MIN_POSITIVE {
773 return None; }
775 let t = ((a.x - p0.x) * s.y - (a.y - p0.y) * s.x) / denom;
776 let u = ((a.x - p0.x) * d.y - (a.y - p0.y) * d.x) / denom;
777 if t >= -1e-9 && t <= 1.0 + 1e-9 && u >= -1e-9 && u <= 1.0 + 1e-9 {
778 Some(t.clamp(0.0, 1.0))
779 } else {
780 None
781 }
782}
783
784fn locate_face(arr: &Arrangement, p: Point2) -> Option<usize> {
786 for (fi, f) in arr.faces.iter().enumerate() {
787 if f.boundary.len() >= 3 && point_in_polygon(p, &f.boundary) {
788 return Some(fi);
789 }
790 }
791 None
792}
793
794pub fn zone_traversal_oracle(arr: &Arrangement, query: &Line2, samples: usize) -> Vec<usize> {
803 let (q_start, q_dir) = if query.is_vertical {
804 (
805 Point2::new(query.x_const, arr.bbox_min.y - 1.0),
806 Point2::new(0.0, arr.bbox_max.y - arr.bbox_min.y + 2.0),
807 )
808 } else {
809 let x0 = arr.bbox_min.x - 1.0;
810 let x1 = arr.bbox_max.x + 1.0;
811 (
812 Point2::new(x0, query.y_at(x0)),
813 Point2::new(x1 - x0, query.y_at(x1) - query.y_at(x0)),
814 )
815 };
816
817 let mut zone: Vec<usize> = Vec::new();
818 for i in 0..samples {
819 let t = i as f64 / (samples - 1).max(1) as f64;
820 let p = Point2::new(q_start.x + t * q_dir.x, q_start.y + t * q_dir.y);
821 if let Some(fi) = locate_face(arr, p) {
822 if zone.last() != Some(&fi) {
823 zone.push(fi);
824 }
825 }
826 }
827 zone
828}
829
830pub fn dual_point_to_line(p: Point2) -> Line2 {
839 Line2::new(p.x, -p.y)
840}
841
842pub fn dual_line_to_point(l: &Line2) -> Option<Point2> {
846 if l.is_vertical {
847 return None;
848 }
849 Some(Point2::new(l.slope, -l.intercept))
850}
851
852pub fn dual_round_trip(p: Point2) -> Point2 {
855 let line = dual_point_to_line(p);
856 dual_line_to_point(&line).unwrap_or(p)
857}
858
859pub fn dual_incidence_holds(p: Point2, l: &Line2) -> bool {
862 if l.is_vertical {
863 return true;
865 }
866 let p_star = dual_point_to_line(p);
867 let l_star = dual_line_to_point(l).unwrap();
868 let p_on_l = (p.y - l.y_at(p.x)).abs() < 1e-9 * (p.y.abs() + 1.0);
870 let p_star_through_l_star =
872 (l_star.y - p_star.y_at(l_star.x)).abs() < 1e-9 * (l_star.y.abs() + 1.0);
873 p_on_l == p_star_through_l_star
874}
875
876#[cfg(test)]
881mod tests {
882 use super::*;
883
884 fn two_general_lines() -> Vec<Line2> {
886 vec![Line2::new(1.0, 0.0), Line2::new(-1.0, 2.0)]
887 }
888
889 fn three_general_lines() -> Vec<Line2> {
891 vec![
892 Line2::new(1.0, 0.0), Line2::new(-1.0, 2.0), Line2::new(0.0, 1.0), ]
896 }
897
898 #[test]
899 fn rejects_too_few_lines() {
900 assert_eq!(
901 build_line_arrangement(&[Line2::new(1.0, 0.0)]),
902 Err(ArrangementError::TooFewLines { got: 1 })
903 );
904 }
905
906 #[test]
907 fn rejects_all_parallel() {
908 let lines = vec![
909 Line2::new(1.0, 0.0),
910 Line2::new(1.0, 1.0),
911 Line2::new(1.0, 2.0),
912 ];
913 assert_eq!(
914 build_line_arrangement(&lines),
915 Err(ArrangementError::AllParallel)
916 );
917 }
918
919 #[test]
920 fn two_lines_vef_counts() {
921 let arr = build_line_arrangement(&two_general_lines()).unwrap();
924 let c = arr.counts();
925 assert_eq!(
926 c.euler, 2,
927 "Euler V-E+F must be 2, got V={} E={} F={}",
928 c.vertices, c.edges, c.faces
929 );
930 }
931
932 #[test]
933 fn three_lines_vef_counts() {
934 let arr = build_line_arrangement(&three_general_lines()).unwrap();
937 let c = arr.counts();
938 assert_eq!(
939 c.euler, 2,
940 "Euler V-E+F must be 2, got V={} E={} F={}",
941 c.vertices, c.edges, c.faces
942 );
943 }
944
945 #[test]
946 fn euler_identity_holds_for_general_position() {
947 for n in 2..=6 {
952 let lines: Vec<Line2> = (0..n)
953 .map(|i| Line2::new((i as f64 + 1.0) * 0.3, (i as f64) * 0.7))
954 .collect();
955 let arr = build_line_arrangement(&lines).unwrap();
956 let c = arr.counts();
957 assert_eq!(
958 c.euler, 2,
959 "Euler V-E+F must be 2 for {n} lines (got V={}, E={}, F={})",
960 c.vertices, c.edges, c.faces
961 );
962 }
963 }
964
965 #[test]
966 fn concurrent_lines_dedup_vertices() {
967 let lines = vec![
969 Line2::new(1.0, 0.0),
970 Line2::new(-1.0, 0.0),
971 Line2::new(2.0, 0.0),
972 ];
973 let arr = build_line_arrangement(&lines).unwrap();
974 assert!(
976 arr.vertices
977 .iter()
978 .any(|v| (v.x - 0.0).abs() < 1e-9 && (v.y - 0.0).abs() < 1e-9),
979 "concurrent lines must include the origin in vertices"
980 );
981 assert_eq!(arr.counts().euler, 2);
982 }
983
984 #[test]
985 fn vertical_line_handled() {
986 let lines = vec![Line2::vertical(0.0), Line2::new(1.0, 0.0)];
988 let arr = build_line_arrangement(&lines).unwrap();
989 assert!(
991 arr.vertices
992 .iter()
993 .any(|v| (v.x - 0.0).abs() < 1e-9 && (v.y - 0.0).abs() < 1e-9),
994 "vertical + non-vertical must include the origin"
995 );
996 assert_eq!(arr.counts().euler, 2);
997 }
998
999 #[test]
1000 fn zone_traversal_matches_oracle_two_lines() {
1001 let arr = build_line_arrangement(&two_general_lines()).unwrap();
1002 let query = Line2::new(0.5, 0.5);
1003 let zone = zone_traversal(&arr, &query);
1004 let oracle = zone_traversal_oracle(&arr, &query, 1000);
1005 assert_eq!(
1008 zone.len(),
1009 oracle.len(),
1010 "zone length {} should match oracle {}",
1011 zone.len(),
1012 oracle.len()
1013 );
1014 for i in 0..zone.len() {
1015 assert_eq!(
1016 zone[i], oracle[i],
1017 "zone face {i} mismatch: zone={} vs oracle={}",
1018 zone[i], oracle[i]
1019 );
1020 }
1021 }
1022
1023 #[test]
1024 fn zone_traversal_matches_oracle_three_lines() {
1025 let arr = build_line_arrangement(&three_general_lines()).unwrap();
1026 let query = Line2::new(0.3, 0.7);
1027 let zone = zone_traversal(&arr, &query);
1028 let oracle = zone_traversal_oracle(&arr, &query, 2000);
1029 assert_eq!(zone, oracle, "zone traversal must match brute-force oracle");
1030 }
1031
1032 #[test]
1033 fn zone_traversal_matches_oracle_five_lines() {
1034 let lines: Vec<Line2> = (0..5)
1035 .map(|i| Line2::new((i as f64 + 1.0) * 0.4, (i as f64) * 0.5))
1036 .collect();
1037 let arr = build_line_arrangement(&lines).unwrap();
1038 let query = Line2::new(0.7, 0.3);
1039 let zone = zone_traversal(&arr, &query);
1040 let oracle = zone_traversal_oracle(&arr, &query, 5000);
1041 assert_eq!(zone, oracle, "zone traversal must match oracle for 5 lines");
1042 }
1043
1044 #[test]
1045 fn zone_traversal_vertical_query() {
1046 let arr = build_line_arrangement(&three_general_lines()).unwrap();
1047 let query = Line2::vertical(0.5);
1048 let zone = zone_traversal(&arr, &query);
1049 let oracle = zone_traversal_oracle(&arr, &query, 2000);
1050 assert_eq!(zone, oracle, "vertical query zone must match oracle");
1051 }
1052
1053 #[test]
1054 fn zone_theorem_bound_holds() {
1055 for n in 2..=6 {
1058 let lines: Vec<Line2> = (0..n)
1059 .map(|i| Line2::new((i as f64 + 1.0) * 0.3, (i as f64) * 0.7))
1060 .collect();
1061 let arr = build_line_arrangement(&lines).unwrap();
1062 let query = Line2::new(0.5, 0.5);
1063 let zone = zone_traversal(&arr, &query);
1064 assert!(
1065 zone.len() <= 2 * n + 1,
1066 "zone of {n} lines has {} faces, should be ≤ {}",
1067 zone.len(),
1068 2 * n + 1
1069 );
1070 }
1071 }
1072
1073 #[test]
1076 fn dual_point_to_line_correct() {
1077 let p = Point2::new(2.0, 3.0);
1079 let l = dual_point_to_line(p);
1080 assert!(!l.is_vertical);
1081 assert_eq!(l.slope, 2.0);
1082 assert_eq!(l.intercept, -3.0);
1083 }
1084
1085 #[test]
1086 fn dual_line_to_point_correct() {
1087 let l = Line2::new(2.0, 3.0);
1089 let p = dual_line_to_point(&l).unwrap();
1090 assert_eq!(p, Point2::new(2.0, -3.0));
1091 }
1092
1093 #[test]
1094 fn dual_vertical_line_has_no_point() {
1095 let l = Line2::vertical(1.0);
1096 assert!(dual_line_to_point(&l).is_none());
1097 }
1098
1099 #[test]
1100 fn dual_round_trip_finite_non_vertical() {
1101 for &(a, b) in &[
1103 (0.0, 0.0),
1104 (1.0, 2.0),
1105 (-3.5, 7.2),
1106 (1e6, -1e-6),
1107 (0.001, -0.001),
1108 ] {
1109 let p = Point2::new(a, b);
1110 let rt = dual_round_trip(p);
1111 assert!(
1112 (rt.x - p.x).abs() < 1e-12 && (rt.y - p.y).abs() < 1e-12,
1113 "round-trip failed for ({a}, {b}): got ({}, {})",
1114 rt.x,
1115 rt.y
1116 );
1117 }
1118 }
1119
1120 #[test]
1121 fn dual_above_below_property() {
1122 let l = Line2::new(1.0, 0.0); let l_star = dual_line_to_point(&l).unwrap(); let p_above = Point2::new(0.0, 1.0);
1128 let p_above_star = dual_point_to_line(p_above); let p_above_on_l = (p_above.y - l.y_at(p_above.x)).abs() < 1e-12;
1130 let l_star_above_p_star = l_star.y > p_above_star.y_at(l_star.x);
1131 assert!(!p_above_on_l, "p_above should not be on l");
1132 assert!(
1133 l_star_above_p_star,
1134 "l* should be above p* when p is above l"
1135 );
1136
1137 let p_below = Point2::new(0.0, -1.0);
1139 let p_below_star = dual_point_to_line(p_below); let l_star_below_p_star = l_star.y < p_below_star.y_at(l_star.x);
1141 assert!(
1142 l_star_below_p_star,
1143 "l* should be below p* when p is below l"
1144 );
1145 }
1146
1147 #[test]
1148 fn dual_incidence_preserved() {
1149 let l = Line2::new(2.0, 1.0); let p = Point2::new(1.0, 3.0); assert!(
1153 dual_incidence_holds(p, &l),
1154 "incidence must hold for p on l"
1155 );
1156
1157 let p_off = Point2::new(1.0, 4.0); assert!(
1160 dual_incidence_holds(p_off, &l),
1161 "incidence must hold (both false) for p not on l"
1162 );
1163 }
1164
1165 #[test]
1166 fn determinism_same_input_same_output() {
1167 let lines = three_general_lines();
1168 let a1 = build_line_arrangement(&lines).unwrap();
1169 let a2 = build_line_arrangement(&lines).unwrap();
1170 assert_eq!(a1.vertices, a2.vertices);
1171 assert_eq!(a1.edges, a2.edges);
1172 assert_eq!(a1.faces, a2.faces);
1173 }
1174
1175 #[test]
1176 fn line_through_points_correct() {
1177 let a = Point2::new(0.0, 1.0);
1178 let b = Point2::new(2.0, 5.0);
1179 let l = Line2::through_points(a, b);
1180 assert_eq!(l.slope, 2.0);
1181 assert_eq!(l.intercept, 1.0);
1182 assert!(!l.is_vertical);
1183 }
1184
1185 #[test]
1186 fn line_through_vertical_points() {
1187 let a = Point2::new(3.0, 0.0);
1188 let b = Point2::new(3.0, 5.0);
1189 let l = Line2::through_points(a, b);
1190 assert!(l.is_vertical);
1191 assert_eq!(l.x_const, 3.0);
1192 }
1193
1194 #[test]
1195 fn parallel_detection() {
1196 assert!(Line2::new(1.0, 0.0).is_parallel(&Line2::new(1.0, 5.0)));
1197 assert!(!Line2::new(1.0, 0.0).is_parallel(&Line2::new(2.0, 0.0)));
1198 assert!(Line2::vertical(1.0).is_parallel(&Line2::vertical(2.0)));
1199 assert!(!Line2::vertical(1.0).is_parallel(&Line2::new(1.0, 0.0)));
1200 }
1201
1202 #[test]
1203 fn arrangement_with_parallel_lines() {
1204 let lines = vec![
1206 Line2::new(1.0, 0.0),
1207 Line2::new(1.0, 2.0), Line2::new(-1.0, 3.0), ];
1210 let arr = build_line_arrangement(&lines).unwrap();
1211 assert_eq!(arr.counts().euler, 2);
1215 }
1216}