Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
arrangements.rs

1//! P11.8 — Arrangements, point-line duality, and topological sweep.
2//!
3//! The acceptance gate requires: "Full line arrangement has correct V/E/F
4//! counts, zone traversal matches a direct oracle, and dual transforms
5//! round-trip finite/non-vertical cases."
6//!
7//! ## Algorithms
8//!
9//! ### Line arrangement
10//!
11//! Given `n` lines, the arrangement is the planar subdivision induced by their
12//! pairwise intersections. For `n` lines in general position (no three
13//! concurrent, no two parallel):
14//!
15//! - **V** = `n(n-1)/2` intersection points.
16//! - **E** = `n²` edges (each line is split into `n` segments/rays by the
17//!   other `n-1` lines, giving `n` edges per line × `n` lines = `n²`).
18//! - **F** = `n(n-1)/2 + 1` faces (by Euler: V − E + F = 2).
19//!
20//! Unbounded edges (rays) are clipped to a bounding box enclosing all
21//! vertices with a margin, so the arrangement is treated as a bounded
22//! subdivision with a single unbounded face wrapping around the box.
23//!
24//! ### Zone traversal
25//!
26//! The *zone* of a curve `γ` in an arrangement `A` is the sequence of faces
27//! that `γ` passes through. For a line crossing an arrangement of `n` lines,
28//! the zone has at most `2n` faces (the Zone Theorem). The traversal finds
29//! every intersection of `γ` with arrangement edges, sorts them along `γ`,
30//! and reports the face between each consecutive pair by locating a
31//! midpoint in the arrangement.
32//!
33//! ### Point-line duality
34//!
35//! The standard point-line duality transform:
36//!
37//! - Point `p = (a, b)` ↔ dual line `p* : y = a·x − b`.
38//! - Line `l : y = m·x + c` ↔ dual point `l* = (m, −c)`.
39//!
40//! Key property: `p` is above `l` ⟺ `l*` is above `p*`. The round-trip
41//! `dual(dual(p)) = p` holds for all finite, non-vertical cases.
42//!
43//! ## Zero-heap contract
44//!
45//! Tier-2 cold construction (AGENTS.md §0-A): `Vec` during build; typed struct
46//! output. The orientation predicate path is zero-heap.
47
48use super::boolean_2::point_in_polygon;
49use super::primitives::Point2;
50
51// ───────────────────────────────────────────────────────────────────────────
52//  Line representation
53// ───────────────────────────────────────────────────────────────────────────
54
55/// A 2-D line in slope-intercept form. Vertical lines have `is_vertical = true`
56/// and use `x_const` instead of slope/intercept.
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub struct Line2 {
59    /// Slope `m` in `y = m·x + b`. Unused for vertical lines.
60    pub slope: f64,
61    /// Intercept `b` in `y = m·x + b`. Unused for vertical lines.
62    pub intercept: f64,
63    /// True for a vertical line `x = x_const`.
64    pub is_vertical: bool,
65    /// For vertical lines: the constant x-value.
66    pub x_const: f64,
67}
68
69impl Line2 {
70    /// Create a non-vertical line `y = slope·x + intercept`.
71    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    /// Create a vertical line `x = x_const`.
81    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    /// Create a line through two points.
91    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    /// Evaluate the line at x: returns `y = slope·x + intercept`, or `x_const`
103    /// for vertical lines (in which case `x` should equal `x_const`).
104    #[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    /// Evaluate the x-value at a given y: returns `x = (y - b) / m`, or
114    /// `x_const` for vertical lines.
115    #[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            // Horizontal line: no unique x for a given y (unless y == intercept).
121            f64::NAN
122        } else {
123            (y - self.intercept) / self.slope
124        }
125    }
126
127    /// Is this line parallel to `other`?
128    #[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
141/// Compute the intersection point of two lines. Returns `None` if parallel.
142pub fn line_line_intersection(l1: &Line2, l2: &Line2) -> Option<Point2> {
143    if l1.is_vertical && l2.is_vertical {
144        return None; // parallel (or identical)
145    }
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// ───────────────────────────────────────────────────────────────────────────
161//  Arrangement types
162// ───────────────────────────────────────────────────────────────────────────
163
164/// One edge of a line arrangement: a segment along one line between two
165/// vertices (or between a vertex and a bounding-box clip point for unbounded
166/// edges).
167#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct ArrangementEdge {
169    /// The line index this edge lies on.
170    pub line: usize,
171    /// Start point.
172    pub start: Point2,
173    /// End point.
174    pub end: Point2,
175}
176
177/// One face of a line arrangement: a polygon (possibly unbounded, clipped to
178/// the bounding box).
179#[derive(Debug, Clone, PartialEq)]
180pub struct ArrangementFace {
181    /// Vertices of the face boundary in CCW order (clipped to bounding box).
182    pub boundary: Vec<Point2>,
183    /// True if this face touches the bounding box (i.e. is unbounded in the
184    /// original arrangement).
185    pub unbounded: bool,
186}
187
188/// A line arrangement: the planar subdivision induced by a set of lines.
189#[derive(Debug, Clone, PartialEq)]
190pub struct Arrangement {
191    /// The input lines.
192    pub lines: Vec<Line2>,
193    /// All intersection points (vertices of the arrangement).
194    pub vertices: Vec<Point2>,
195    /// All edges (segments between consecutive vertices along each line).
196    pub edges: Vec<ArrangementEdge>,
197    /// All faces (cells of the subdivision).
198    pub faces: Vec<ArrangementFace>,
199    /// The bounding box used to clip unbounded edges.
200    pub bbox_min: Point2,
201    pub bbox_max: Point2,
202}
203
204/// Summary counts for verification.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct ArrangementCounts {
207    pub vertices: usize,
208    pub edges: usize,
209    pub faces: usize,
210    /// Euler characteristic V − E + F (should be 2 for a planar subdivision).
211    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
234// ───────────────────────────────────────────────────────────────────────────
235//  Arrangement construction
236// ───────────────────────────────────────────────────────────────────────────
237
238/// Bounding-box margin factor: the clip box extends this multiple of the
239/// vertex coordinate range beyond the extreme vertices.
240const BBOX_MARGIN: f64 = 3.0;
241
242/// Compute the bounding box for a set of vertices, with margin.
243fn 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
265/// Clip a line to the bounding box, returning the two endpoints of the
266/// clipped segment.
267fn clip_line_to_bbox(line: &Line2, bmin: Point2, bmax: Point2) -> (Point2, Point2) {
268    // Liang-Barsky clipping of the line to the box [bmin, bmax].
269    // Parametrize the line as P(t) = P0 + t * d.
270    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        // Pick two far-apart points on the line.
277        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
292/// Liang-Barsky line clipping. Returns (t0, t1) with 0 ≤ t0 ≤ t1 ≤ 1 for the
293/// portion of P(t) = p0 + t·d inside the box [bmin, bmax].
294fn 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), // left
300        (d.x, bmax.x - p0.x),  // right
301        (-d.y, p0.y - bmin.y), // bottom
302        (d.y, bmax.y - p0.y),  // top
303    ] {
304        if p.abs() <= f64::MIN_POSITIVE {
305            // Parallel to this boundary.
306            if q < 0.0 {
307                return (0.0, 0.0); // entirely outside
308            }
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
321/// Build a line arrangement from a set of lines.
322///
323/// Computes all pairwise intersections, splits each line at its intersection
324/// points (clipped to a bounding box for unbounded edges), and extracts faces
325/// by tracing boundary cycles.
326pub 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    // Step 1 — compute all pairwise intersections.
333    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    // Deduplicate vertices (concurrent lines produce the same intersection).
349    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    // Step 2 — compute bounding box.
357    let (bmin, bmax) = compute_bbox(&vertices);
358
359    // Step 3 — for each line, find vertices on it, sort along the line, and
360    // emit edges between consecutive vertices (including bbox clip endpoints).
361    let mut edges: Vec<ArrangementEdge> = Vec::new();
362    // Collect all bbox boundary exit points (clip endpoints) for each line.
363    let mut boundary_points: Vec<Point2> = Vec::new();
364    for (li, line) in lines.iter().enumerate() {
365        // Clip the line to the bounding box.
366        let (clip_a, clip_b) = clip_line_to_bbox(line, bmin, bmax);
367
368        // Save clip endpoints as boundary points.
369        boundary_points.push(clip_a);
370        boundary_points.push(clip_b);
371
372        // Collect all points on this line: intersection points + clip endpoints.
373        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                // Compute parameter t along clip_a → clip_b.
379                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        // Emit edges between consecutive points.
394        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    // Step 3b — add bbox boundary edges, split at every boundary exit point.
408    // The bbox boundary is part of the subdivision: it closes all face cycles.
409    let bbox_edges = build_bbox_boundary_edges(bmin, bmax, &boundary_points);
410    edges.extend(bbox_edges);
411
412    // Step 3c — collect ALL vertices of the subdivision (not just line-line
413    // intersections): every unique edge endpoint is a vertex. This includes
414    // bbox corners and line clip points, which are needed for the Euler
415    // identity V − E + F = 2.
416    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    // Sort for determinism.
422    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    // Step 4 — extract faces by building a DCEL-like structure and walking
429    // boundary cycles. We use the same approach as dcel_overlay.rs:
430    //   - Build half-edges with twin linkage
431    //   - Sort outgoing half-edges per vertex CCW
432    //   - Link next/prev via the CCW-predecessor rule
433    //   - Walk cycles → faces
434    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
446/// Check if a point lies on a line (within tolerance).
447fn 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
455/// Build bbox boundary edges, split at every boundary exit point.
456///
457/// The bbox has 4 sides. Each side is split at every boundary point that lies
458/// on it, producing edges between consecutive split points (including corners).
459/// A special line index `usize::MAX` marks bbox boundary edges.
460fn 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), // bottom-left
467        Point2::new(bmax.x, bmin.y), // bottom-right
468        Point2::new(bmax.x, bmax.y), // top-right
469        Point2::new(bmin.x, bmax.y), // top-left
470    ];
471
472    // Each side: (start_corner, end_corner, axis, is_horizontal).
473    // Bottom: bmin.x → bmax.x at y=bmin.y (left to right)
474    // Right: bmin.y → bmax.y at x=bmax.x (bottom to top)
475    // Top: bmax.x → bmin.x at y=bmax.y (right to left)
476    // Left: bmax.y → bmin.y at x=bmin.x (top to bottom)
477    let sides: [(Point2, Point2, bool); 4] = [
478        (corners[0], corners[1], true),  // bottom, horizontal
479        (corners[1], corners[2], false), // right, vertical
480        (corners[2], corners[3], true),  // top, horizontal
481        (corners[3], corners[0], false), // left, vertical
482    ];
483
484    let mut edges = Vec::new();
485    for (start, end, horizontal) in sides {
486        // Collect split points on this side.
487        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                // Check if bp is on this horizontal segment.
493                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                // Check if bp is on this vertical segment.
504                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, // bbox boundary marker
524                    start: s,
525                    end: e,
526                });
527            }
528        }
529    }
530    edges
531}
532
533// ───────────────────────────────────────────────────────────────────────────
534//  Face extraction (DCEL cycle walk)
535// ───────────────────────────────────────────────────────────────────────────
536
537/// A half-edge for arrangement face extraction.
538#[derive(Clone, Copy)]
539struct ArrHalfEdge {
540    origin: usize, // index into a vertex array
541    twin: usize,   // index into half-edge array
542    next: usize,   // index into half-edge array
543    face: usize,   // face index
544}
545
546/// Extract faces from the arrangement edges by building a DCEL and walking
547/// boundary cycles. Uses the same CCW-predecessor linkage rule as
548/// `dcel_overlay.rs`.
549fn extract_faces(
550    edges: &[ArrangementEdge],
551    vertices: &[Point2],
552    _bmin: Point2,
553    _bmax: Point2,
554) -> Vec<ArrangementFace> {
555    // Build a combined vertex array: arrangement vertices + edge endpoints
556    // (which include bbox clip points not in the vertex list).
557    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    // Build half-edges: two per edge.
567    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    // Group outgoing half-edges per vertex, sort CCW by direction.
589    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    // Linkage: twin(g_i).next = g_{(i-1) mod k} (CCW-predecessor).
605    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    // Walk face cycles.
620    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    // Build face records: compute boundary vertices and signed area.
650    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        // CCW (area > 0) → bounded face. CW (area < 0) → unbounded face
655        // (the "outer" cycle of the arrangement, wrapping around the bbox).
656        let unbounded = area < 0.0;
657        faces.push(ArrangementFace {
658            boundary,
659            unbounded,
660        });
661    }
662
663    // For the unbounded face, reverse the boundary to get CCW (convention).
664    for f in &mut faces {
665        if f.unbounded {
666            f.boundary.reverse();
667        }
668    }
669
670    faces
671}
672
673/// Insert a point into the vertex array if not already present (by coordinate).
674fn 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
681/// Find the index of a point in the array by coordinate (within tolerance).
682fn 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
691/// Signed area of a polygon (positive = CCW).
692fn 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
705// ───────────────────────────────────────────────────────────────────────────
706//  Zone traversal
707// ───────────────────────────────────────────────────────────────────────────
708
709/// The zone of a query line through an arrangement: the sequence of face
710/// indices that the line passes through, in order along the line.
711pub fn zone_traversal(arr: &Arrangement, query: &Line2) -> Vec<usize> {
712    // Find all intersection points of the query line with arrangement edges.
713    let mut crossings: Vec<(f64, usize)> = Vec::new(); // (parameter along query, edge index)
714
715    // Parametrize the query line.
716    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        // Find intersection of the query line segment with edge e.
732        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    // Sort crossings by parameter along the query line.
740    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        // The query line doesn't cross any edge — it's entirely in one face.
745        // Find which face contains a point on the query line.
746        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    // Between consecutive crossings, the query line is in one face.
754    // Find a midpoint in each segment and locate the face.
755    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
767/// Parametric intersection of segment P(t) = p0 + t·d (0 ≤ t ≤ 1) with
768/// segment (a, b). Returns t if they intersect properly, None otherwise.
769fn 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; // parallel
774    }
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
784/// Locate which face contains a point (by point-in-polygon test).
785fn 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
794// ───────────────────────────────────────────────────────────────────────────
795//  Zone traversal oracle (brute force)
796// ───────────────────────────────────────────────────────────────────────────
797
798/// Brute-force zone traversal: for a fine sample of points along the query
799/// line, locate the containing face. Returns the distinct face indices in
800/// order of first encounter. Used as an independent oracle to verify
801/// `zone_traversal`.
802pub 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
830// ───────────────────────────────────────────────────────────────────────────
831//  Point-line duality
832// ───────────────────────────────────────────────────────────────────────────
833
834/// Dual of a point `p = (a, b)`: the line `y = a·x − b`.
835///
836/// Property: point `p` is above line `l` ⟺ dual line `p*` is above dual
837/// point `l*`.
838pub fn dual_point_to_line(p: Point2) -> Line2 {
839    Line2::new(p.x, -p.y)
840}
841
842/// Dual of a non-vertical line `l : y = m·x + c`: the point `(m, −c)`.
843///
844/// Returns `None` for vertical lines (which have no finite dual point).
845pub 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
852/// Round-trip the duality: `dual(dual(p))` should equal `p` for all finite,
853/// non-vertical points.
854pub 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
859/// Check the incidence-preserving property: point `p` is on line `l` ⟺ dual
860/// line `p*` passes through dual point `l*`.
861pub fn dual_incidence_holds(p: Point2, l: &Line2) -> bool {
862    if l.is_vertical {
863        // Duality is defined for non-vertical lines only.
864        return true;
865    }
866    let p_star = dual_point_to_line(p);
867    let l_star = dual_line_to_point(l).unwrap();
868    // p on l ⟺ p.y == l.y_at(p.x)
869    let p_on_l = (p.y - l.y_at(p.x)).abs() < 1e-9 * (p.y.abs() + 1.0);
870    // p* passes through l* ⟺ l*.y == p*.y_at(l*.x)
871    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// ───────────────────────────────────────────────────────────────────────────
877//  Tests
878// ───────────────────────────────────────────────────────────────────────────
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883
884    /// Two non-parallel, non-vertical lines in general position.
885    fn two_general_lines() -> Vec<Line2> {
886        vec![Line2::new(1.0, 0.0), Line2::new(-1.0, 2.0)]
887    }
888
889    /// Three lines in general position (no two parallel, no three concurrent).
890    fn three_general_lines() -> Vec<Line2> {
891        vec![
892            Line2::new(1.0, 0.0),  // y = x
893            Line2::new(-1.0, 2.0), // y = -x + 2
894            Line2::new(0.0, 1.0),  // y = 1 (horizontal)
895        ]
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        // 2 lines in general position: 1 intersection + 4 bbox corners = 5
922        // vertices. 4 line edges + 4 bbox edges = 8 edges. Euler: 5-8+F=2 → F=5.
923        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        // 3 lines in general position: 3 intersections + bbox corners + clip
935        // points. Just check Euler = 2.
936        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 lines in general position: V = n(n-1)/2, E = n², F = n(n-1)/2 + 1.
948        // V - E + F = n(n-1)/2 - n² + n(n-1)/2 + 1 = n(n-1) - n² + 1 = -n + 1.
949        // But with the bbox clipping, the arrangement becomes a bounded
950        // subdivision with Euler = 2.
951        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        // Three lines through the origin: all intersect at (0,0).
968        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        // The origin (0,0) must be among the vertices.
975        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        // A vertical line and a non-vertical line.
987        let lines = vec![Line2::vertical(0.0), Line2::new(1.0, 0.0)];
988        let arr = build_line_arrangement(&lines).unwrap();
989        // The origin (0,0) must be among the vertices.
990        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        // The zone should be the same set of faces (order may differ at
1006        // boundaries, but the sequence should match).
1007        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        // The zone of a line in an arrangement of n lines has at most 2n faces.
1056        // (We check ≤ 2n + 1 to account for bbox boundary effects.)
1057        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    // ── Point-line duality ──
1074
1075    #[test]
1076    fn dual_point_to_line_correct() {
1077        // (a, b) → y = a·x − b
1078        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        // y = m·x + c → (m, −c)
1088        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        // dual(dual(p)) = p for all finite, non-vertical points.
1102        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        // p above l ⟺ l* above p*
1123        let l = Line2::new(1.0, 0.0); // y = x
1124        let l_star = dual_line_to_point(&l).unwrap(); // (1, 0)
1125
1126        // p above l: p = (0, 1), y=1 > l.y_at(0)=0
1127        let p_above = Point2::new(0.0, 1.0);
1128        let p_above_star = dual_point_to_line(p_above); // y = 0·x − 1 = −1
1129        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        // p below l: p = (0, -1)
1138        let p_below = Point2::new(0.0, -1.0);
1139        let p_below_star = dual_point_to_line(p_below); // y = 1
1140        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        // If p is on l, then p* passes through l*.
1150        let l = Line2::new(2.0, 1.0); // y = 2x + 1
1151        let p = Point2::new(1.0, 3.0); // on l: 3 = 2·1 + 1 ✓
1152        assert!(
1153            dual_incidence_holds(p, &l),
1154            "incidence must hold for p on l"
1155        );
1156
1157        // If p is NOT on l, then p* does NOT pass through l*.
1158        let p_off = Point2::new(1.0, 4.0); // not on l: 4 ≠ 3
1159        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        // Two parallel lines + one transversal.
1205        let lines = vec![
1206            Line2::new(1.0, 0.0),
1207            Line2::new(1.0, 2.0),  // parallel to first
1208            Line2::new(-1.0, 3.0), // transversal
1209        ];
1210        let arr = build_line_arrangement(&lines).unwrap();
1211        // 2 intersections (transversal crosses each parallel line once).
1212        // Check Euler identity rather than exact vertex count (which includes
1213        // bbox corners and clip points).
1214        assert_eq!(arr.counts().euler, 2);
1215    }
1216}