Skip to main content

qualia_core_db/specialized_libs/computational_geometry/
kirkpatrick.rs

1//! Kirkpatrick hierarchy for O(log n) point location in planar subdivisions
2//! (P11.7).
3//!
4//! Given a triangulated planar subdivision, the Kirkpatrick hierarchy builds
5//! a sequence of progressively coarser triangulations by removing independent
6//! sets of low-degree vertices and retriangulating the holes. Point location
7//! starts at the coarsest level (a single triangle) and refines downward:
8//! at each level, the triangle containing the query is found by checking
9//! the (constant-size) set of triangles that replaced it.
10//!
11//! **Guaranteed** O(log n) query time and O(n) space — no randomization.
12//!
13//! Reference: Kirkpatrick, "Optimal search in planar subdivisions,"
14//! *SIAM J. Comput.* 1983. Also de Berg et al. §6.3 (simplified variant).
15//!
16//! Tier-2 cold construction (uses `Vec` during build; query is allocation-free).
17
18use super::primitives::{orientation_2, Orientation, Point2};
19
20// ───────────────────────────────────────────────────────────────────────────
21//  Error type
22// ───────────────────────────────────────────────────────────────────────────
23
24/// Error returned by Kirkpatrick hierarchy operations.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum KirkpatrickError {
27    /// Triangulation is empty.
28    EmptyTriangulation,
29    /// Fewer than 3 vertices.
30    TooFewVertices,
31    /// Query point is outside the bounding triangle.
32    OutsideBoundingTriangle,
33}
34
35impl core::fmt::Display for KirkpatrickError {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        match self {
38            Self::EmptyTriangulation => write!(f, "kirkpatrick: empty triangulation"),
39            Self::TooFewVertices => write!(f, "kirkpatrick: need at least 3 vertices"),
40            Self::OutsideBoundingTriangle => {
41                write!(f, "kirkpatrick: query outside bounding triangle")
42            }
43        }
44    }
45}
46
47impl std::error::Error for KirkpatrickError {}
48
49// ───────────────────────────────────────────────────────────────────────────
50//  Triangulation representation
51// ───────────────────────────────────────────────────────────────────────────
52
53/// A triangle in the hierarchy: three vertex indices + the face label it
54/// represents (for the finest level, this is the original face index; for
55/// coarser levels, it's a sentinel).
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57struct Tri {
58    v: [usize; 3],
59    /// Original face index at the finest level, or `usize::MAX` for
60    /// triangles introduced during hole retriangulation.
61    face: usize,
62    /// Level at which this triangle exists.
63    level: usize,
64}
65
66/// A level of the hierarchy: a set of triangles and their vertex positions.
67#[derive(Debug, Clone)]
68struct Level {
69    triangles: Vec<Tri>,
70    /// Map from a triangle index at this level to the triangle indices at
71    /// the *finer* (previous) level that it replaces. Used during refinement.
72    /// `children[i]` = list of finer triangles that overlap triangle `i`.
73    children: Vec<Vec<usize>>,
74}
75
76// ───────────────────────────────────────────────────────────────────────────
77//  Kirkpatrick hierarchy
78// ───────────────────────────────────────────────────────────────────────────
79
80/// Kirkpatrick point-location hierarchy.
81///
82/// Build with [`KirkpatrickHierarchy::build`], query with
83/// [`KirkpatrickHierarchy::locate`].
84pub struct KirkpatrickHierarchy {
85    /// Levels from finest (index 0) to coarsest.
86    levels: Vec<Level>,
87    /// Original vertex positions.
88    vertices: Vec<Point2>,
89    /// Bounding triangle vertex indices (into `vertices`).
90    bbox_tri: [usize; 3],
91}
92
93impl std::fmt::Debug for KirkpatrickHierarchy {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("KirkpatrickHierarchy")
96            .field("num_levels", &self.levels.len())
97            .field("num_vertices", &self.vertices.len())
98            .field("bbox_tri", &self.bbox_tri)
99            .finish()
100    }
101}
102
103/// Orientation test for a triangle: returns true if `p` is inside or on
104/// the boundary of the CCW triangle `(a, b, c)`.
105fn point_in_tri(p: Point2, a: Point2, b: Point2, c: Point2) -> bool {
106    let o1 = orientation_2(a, b, p);
107    let o2 = orientation_2(b, c, p);
108    let o3 = orientation_2(c, a, p);
109    o1 != Orientation::Clockwise && o2 != Orientation::Clockwise && o3 != Orientation::Clockwise
110}
111
112/// Compute a large bounding triangle that contains all points.
113fn bounding_triangle(points: &[Point2]) -> [Point2; 3] {
114    let mut min_x = f64::INFINITY;
115    let mut min_y = f64::INFINITY;
116    let mut max_x = f64::NEG_INFINITY;
117    let mut max_y = f64::NEG_INFINITY;
118    for &p in points {
119        min_x = min_x.min(p.x);
120        min_y = min_y.min(p.y);
121        max_x = max_x.max(p.x);
122        max_y = max_y.max(p.y);
123    }
124    let dx = max_x - min_x;
125    let dy = max_y - min_y;
126    let d = dx.max(dy).max(1.0) * 10.0;
127    let cx = (min_x + max_x) * 0.5;
128    let cy = (min_y + max_y) * 0.5;
129    // Large triangle pointing up.
130    [
131        Point2::new(cx, cy + d),
132        Point2::new(cx - d, cy - d * 0.5),
133        Point2::new(cx + d, cy - d * 0.5),
134    ]
135}
136
137impl KirkpatrickHierarchy {
138    /// Build a Kirkpatrick hierarchy from a triangulation.
139    ///
140    /// `vertices` — the vertex positions.
141    /// `triangles` — list of CCW triangles as vertex index triples.
142    /// `face_labels` — the original face index for each triangle (use
143    ///   `0..triangles.len()` if you just want the triangle index).
144    ///
145    /// The triangulation must cover a simply-connected region (no holes).
146    /// A bounding triangle is added automatically to enclose everything.
147    pub fn build(
148        vertices: &[Point2],
149        triangles: &[[usize; 3]],
150        face_labels: &[usize],
151    ) -> Result<Self, KirkpatrickError> {
152        if vertices.len() < 3 {
153            return Err(KirkpatrickError::TooFewVertices);
154        }
155        if triangles.is_empty() {
156            return Err(KirkpatrickError::EmptyTriangulation);
157        }
158
159        // Add bounding triangle vertices.
160        let bbox = bounding_triangle(vertices);
161        let n = vertices.len();
162        let mut all_vertices = vertices.to_vec();
163        all_vertices.push(bbox[0]);
164        all_vertices.push(bbox[1]);
165        all_vertices.push(bbox[2]);
166        let bv = [n, n + 1, n + 2];
167
168        // Build the finest level: original triangles + bounding triangle
169        // faces. We need to triangulate the gap between the original
170        // triangulation boundary and the bounding triangle. For simplicity,
171        // we fan-triangulate from each bbox vertex to the convex hull
172        // boundary edges. However, computing the exact boundary requires
173        // half-edge analysis. Instead, we use a simpler approach:
174        // we add the bounding triangle as a single face, then connect
175        // each original boundary edge to the nearest bbox vertex.
176        //
177        // Actually, the standard approach is:
178        // 1. Find the boundary edges of the input triangulation.
179        // 2. For each boundary edge, create a triangle connecting it to
180        //    one of the bbox vertices.
181        // 3. The remaining gap (between the fan triangles and the bbox
182        //    triangle) is filled by the bbox triangle itself.
183        //
184        // But this is complex. A simpler approach that works for point
185        // location: just add the bounding triangle as a single face that
186        // covers everything outside the original triangulation. We don't
187        // need a valid triangulation of the gap — we just need the hierarchy
188        // to correctly locate points. The key insight is that the bounding
189        // triangle contains all original points, so any point inside the
190        // bbox triangle but outside the original triangulation will be
191        // "located" to the bbox face.
192        //
193        // For the hierarchy to work, we need a valid triangulation at the
194        // finest level. We create it by:
195        // - Keeping all original triangles.
196        // - Adding triangles from each boundary edge to bbox vertices.
197        // - The bbox triangle itself is the outermost face.
198
199        // Find boundary edges (edges that appear in only one triangle).
200        let mut edge_count: std::collections::HashMap<(usize, usize), usize> =
201            std::collections::HashMap::new();
202        for &t in triangles {
203            for i in 0..3 {
204                let a = t[i];
205                let b = t[(i + 1) % 3];
206                let key = if a < b { (a, b) } else { (b, a) };
207                *edge_count.entry(key).or_insert(0) += 1;
208            }
209        }
210
211        let mut finest_tris: Vec<Tri> = Vec::with_capacity(triangles.len() + 20);
212        for (i, &t) in triangles.iter().enumerate() {
213            finest_tris.push(Tri {
214                v: t,
215                face: face_labels[i],
216                level: 0,
217            });
218        }
219
220        // For each boundary edge, create a triangle to the nearest bbox vertex.
221        // We pick the bbox vertex that makes the triangle CCW.
222        let mut boundary_edges: Vec<(usize, usize)> = Vec::new();
223        for &t in triangles {
224            for i in 0..3 {
225                let a = t[i];
226                let b = t[(i + 1) % 3];
227                let key = if a < b { (a, b) } else { (b, a) };
228                if edge_count[&key] == 1 {
229                    // This is a boundary edge. The triangle t has it in
230                    // CCW order (a→b), so the exterior is on the right.
231                    // We need a CCW triangle (a, bv, b) or (b, bv, a).
232                    // Check which bbox vertex makes it CCW.
233                    for &bv in &bv {
234                        let o = orientation_2(all_vertices[a], all_vertices[b], all_vertices[bv]);
235                        if o == Orientation::CounterClockwise {
236                            boundary_edges.push((a, b));
237                            finest_tris.push(Tri {
238                                v: [a, bv, b],
239                                face: usize::MAX, // exterior face
240                                level: 0,
241                            });
242                            break;
243                        }
244                    }
245                }
246            }
247        }
248
249        // Add the outer bounding triangle (CCW: top → bottom-left → bottom-right).
250        finest_tris.push(Tri {
251            v: [bv[0], bv[1], bv[2]],
252            face: usize::MAX,
253            level: 0,
254        });
255
256        // Build adjacency: for each triangle, its 3 neighbors (across each edge).
257        // We build this by matching directed edges.
258        let level0 = Level {
259            triangles: finest_tris,
260            children: Vec::new(),
261        };
262
263        // Build the hierarchy by repeatedly removing independent sets.
264        let mut levels: Vec<Level> = Vec::new();
265        levels.push(level0.clone());
266
267        const MAX_DEGREE: usize = 12;
268        const MIN_TRIANGLES: usize = 4;
269
270        loop {
271            let current = levels.last().unwrap();
272            if current.triangles.len() <= MIN_TRIANGLES {
273                break;
274            }
275
276            let next = build_next_level(current, &all_vertices, MAX_DEGREE);
277            if next.triangles.len() >= current.triangles.len() {
278                // No progress — stop to avoid infinite loop.
279                break;
280            }
281            levels.push(next);
282        }
283
284        // Build children maps (from coarse to fine).
285        // For each level L > 0, children[i] = list of triangles at level L-1
286        // that overlap triangle i at level L.
287        for li in (1..levels.len()).rev() {
288            let finer = &levels[li - 1].triangles.clone();
289            let coarse = &levels[li].triangles;
290            let mut children = vec![Vec::new(); coarse.len()];
291
292            for (fi, ft) in finer.iter().enumerate() {
293                // Find which coarse triangle contains the centroid of ft.
294                let cx =
295                    (all_vertices[ft.v[0]].x + all_vertices[ft.v[1]].x + all_vertices[ft.v[2]].x)
296                        / 3.0;
297                let cy =
298                    (all_vertices[ft.v[0]].y + all_vertices[ft.v[1]].y + all_vertices[ft.v[2]].y)
299                        / 3.0;
300                let centroid = Point2::new(cx, cy);
301
302                for (ci, ct) in coarse.iter().enumerate() {
303                    if point_in_tri(
304                        centroid,
305                        all_vertices[ct.v[0]],
306                        all_vertices[ct.v[1]],
307                        all_vertices[ct.v[2]],
308                    ) {
309                        children[ci].push(fi);
310                        break;
311                    }
312                }
313            }
314
315            levels[li].children = children;
316        }
317
318        // Level 0 has no children (it's the finest).
319        levels[0].children = vec![Vec::new(); levels[0].triangles.len()];
320
321        Ok(KirkpatrickHierarchy {
322            levels,
323            vertices: all_vertices,
324            bbox_tri: bv,
325        })
326    }
327
328    /// Locate a query point in the hierarchy.
329    ///
330    /// Returns the face label of the containing triangle at the finest level,
331    /// or `None` if the point is in an exterior face (face label `usize::MAX`).
332    /// Returns an error if the point is outside the bounding triangle.
333    pub fn locate(&self, query: Point2) -> Result<Option<usize>, KirkpatrickError> {
334        // Check that the query is inside the bounding triangle.
335        let bv = self.bbox_tri;
336        if !point_in_tri(
337            query,
338            self.vertices[bv[0]],
339            self.vertices[bv[1]],
340            self.vertices[bv[2]],
341        ) {
342            return Err(KirkpatrickError::OutsideBoundingTriangle);
343        }
344
345        // Start at the coarsest level.
346        let coarsest = self.levels.len() - 1;
347        let coarse_tris = &self.levels[coarsest].triangles;
348
349        // Find the containing triangle at the coarsest level.
350        let mut current_tri = 0usize;
351        for (i, t) in coarse_tris.iter().enumerate() {
352            if point_in_tri(
353                query,
354                self.vertices[t.v[0]],
355                self.vertices[t.v[1]],
356                self.vertices[t.v[2]],
357            ) {
358                current_tri = i;
359                break;
360            }
361        }
362
363        // Refine downward through the levels.
364        for li in (0..coarsest).rev() {
365            let children = &self.levels[li + 1].children[current_tri];
366            if children.is_empty() {
367                // No children — we're at the finest level for this branch.
368                // The face is at the current level.
369                let tri = &self.levels[li + 1].triangles[current_tri];
370                return Ok(if tri.face == usize::MAX {
371                    None
372                } else {
373                    Some(tri.face)
374                });
375            }
376
377            // Find which child contains the query.
378            let finer_tris = &self.levels[li].triangles;
379            let mut found = false;
380            for &ci in children {
381                let t = &finer_tris[ci];
382                if point_in_tri(
383                    query,
384                    self.vertices[t.v[0]],
385                    self.vertices[t.v[1]],
386                    self.vertices[t.v[2]],
387                ) {
388                    current_tri = ci;
389                    found = true;
390                    break;
391                }
392            }
393            if !found {
394                // Shouldn't happen if the hierarchy is correct, but fall back.
395                return Ok(None);
396            }
397        }
398
399        // We're at the finest level (level 0).
400        let tri = &self.levels[0].triangles[current_tri];
401        Ok(if tri.face == usize::MAX {
402            None
403        } else {
404            Some(tri.face)
405        })
406    }
407
408    /// Number of levels in the hierarchy.
409    pub fn num_levels(&self) -> usize {
410        self.levels.len()
411    }
412
413    /// Number of triangles at the finest level.
414    pub fn num_finest_triangles(&self) -> usize {
415        self.levels[0].triangles.len()
416    }
417
418    /// Number of triangles at the coarsest level.
419    pub fn num_coarsest_triangles(&self) -> usize {
420        self.levels.last().unwrap().triangles.len()
421    }
422
423    /// Brute-force locate: scan all triangles at the finest level.
424    /// Used as an oracle for testing.
425    pub fn locate_brute_force(&self, query: Point2) -> Option<usize> {
426        for t in &self.levels[0].triangles {
427            if point_in_tri(
428                query,
429                self.vertices[t.v[0]],
430                self.vertices[t.v[1]],
431                self.vertices[t.v[2]],
432            ) {
433                return if t.face == usize::MAX {
434                    None
435                } else {
436                    Some(t.face)
437                };
438            }
439        }
440        None
441    }
442}
443
444/// Build the next coarser level by removing an independent set of
445/// low-degree vertices and retriangulating the holes.
446fn build_next_level(current: &Level, vertices: &[Point2], max_degree: usize) -> Level {
447    // Build vertex → triangles adjacency.
448    let mut vert_tris: Vec<Vec<usize>> = vec![Vec::new(); vertices.len()];
449    for (ti, t) in current.triangles.iter().enumerate() {
450        for &v in &t.v {
451            vert_tris[v].push(ti);
452        }
453    }
454
455    // Find an independent set of vertices with degree <= max_degree.
456    // "Degree" = number of triangles incident to the vertex.
457    // We skip bounding-triangle vertices (the last 3) and any vertex
458    // that shares a triangle with a bbox vertex (i.e., boundary/fan
459    // vertices). Only interior vertices of the original triangulation
460    // are eligible for removal — this ensures the hole is a simple
461    // polygon that can be fan-triangulated.
462    let n_orig = vertices.len() - 3;
463    let mut removed = vec![false; vertices.len()];
464    let mut to_remove: Vec<usize> = Vec::new();
465
466    for v in 0..n_orig {
467        if removed[v] {
468            continue;
469        }
470        if vert_tris[v].len() > max_degree {
471            continue;
472        }
473        // Check that no triangle in the star contains a bbox vertex.
474        let is_interior = vert_tris[v]
475            .iter()
476            .all(|&ti| current.triangles[ti].v.iter().all(|&vv| vv < n_orig));
477        if !is_interior {
478            continue;
479        }
480        to_remove.push(v);
481        removed[v] = true;
482        // Mark all neighbors as unavailable.
483        for &ti in &vert_tris[v] {
484            for &nv in &current.triangles[ti].v {
485                if nv != v && nv < n_orig {
486                    removed[nv] = true;
487                }
488            }
489        }
490    }
491
492    if to_remove.is_empty() {
493        // Can't coarsen — return a copy.
494        return Level {
495            triangles: current.triangles.clone(),
496            children: Vec::new(),
497        };
498    }
499
500    // Remove the selected vertices and retriangulate the holes.
501    // For each removed vertex, collect the "star" (triangles around it),
502    // remove them, and fan-triangulate the hole.
503    let mut new_tris: Vec<Tri> = Vec::new();
504    let mut dead: Vec<bool> = vec![false; current.triangles.len()];
505
506    // Mark triangles that contain a removed vertex as dead.
507    for &v in &to_remove {
508        for &ti in &vert_tris[v] {
509            dead[ti] = true;
510        }
511    }
512
513    // Keep triangles that are not dead.
514    for (ti, t) in current.triangles.iter().enumerate() {
515        if !dead[ti] {
516            new_tris.push(Tri {
517                v: t.v,
518                face: t.face,
519                level: t.level + 1,
520            });
521        }
522    }
523
524    // For each removed vertex, retriangulate its hole.
525    // The hole is the polygon formed by the neighbors of the removed vertex,
526    // in order. We fan-triangulate from the first neighbor.
527    for &v in &to_remove {
528        // Collect the neighbors in CCW order around v.
529        let star = &vert_tris[v];
530        if star.is_empty() {
531            continue;
532        }
533
534        // Build the boundary polygon by tracing the star.
535        let boundary = trace_hole_boundary(star, v, &current.triangles);
536
537        if boundary.len() < 3 {
538            continue;
539        }
540
541        // Fan-triangulate from boundary[0].
542        for i in 1..boundary.len() - 1 {
543            let tri = Tri {
544                v: [boundary[0], boundary[i], boundary[i + 1]],
545                face: usize::MAX,
546                level: current.triangles[0].level + 1,
547            };
548            // Verify CCW; flip if needed.
549            let o = orientation_2(vertices[tri.v[0]], vertices[tri.v[1]], vertices[tri.v[2]]);
550            if o == Orientation::Clockwise {
551                new_tris.push(Tri {
552                    v: [tri.v[0], tri.v[2], tri.v[1]],
553                    face: usize::MAX,
554                    level: tri.level,
555                });
556            } else {
557                new_tris.push(tri);
558            }
559        }
560    }
561
562    Level {
563        triangles: new_tris,
564        children: Vec::new(),
565    }
566}
567
568/// Trace the boundary of the hole left by removing vertex `v` from its
569/// star of triangles. Returns the boundary vertices in CCW order.
570fn trace_hole_boundary(star: &[usize], v: usize, triangles: &[Tri]) -> Vec<usize> {
571    // For each triangle in the star, collect the two edges that don't
572    // contain v. The boundary edges are those that appear in only one
573    // triangle of the star.
574    let mut edge_map: std::collections::HashMap<(usize, usize), usize> =
575        std::collections::HashMap::new();
576
577    for &ti in star {
578        let t = &triangles[ti];
579        for i in 0..3 {
580            let a = t.v[i];
581            let b = t.v[(i + 1) % 3];
582            if a != v && b != v {
583                let key = if a < b { (a, b) } else { (b, a) };
584                *edge_map.entry(key).or_insert(0) += 1;
585            }
586        }
587    }
588
589    // Boundary edges are those with count 1.
590    let mut boundary_edges: Vec<(usize, usize)> = Vec::new();
591    for &ti in star {
592        let t = &triangles[ti];
593        for i in 0..3 {
594            let a = t.v[i];
595            let b = t.v[(i + 1) % 3];
596            if a != v && b != v {
597                let key = if a < b { (a, b) } else { (b, a) };
598                if edge_map[&key] == 1 {
599                    // Keep the directed edge as it appears in the triangle (CCW).
600                    boundary_edges.push((a, b));
601                }
602            }
603        }
604    }
605
606    // Chain the directed edges into a cycle.
607    let mut boundary: Vec<usize> = Vec::new();
608    if boundary_edges.is_empty() {
609        return boundary;
610    }
611
612    let mut used = vec![false; boundary_edges.len()];
613    let mut current = boundary_edges[0];
614    boundary.push(current.0);
615    used[0] = true;
616
617    loop {
618        boundary.push(current.1);
619        // Find the edge starting at current.1 that hasn't been used.
620        let mut found = false;
621        for (i, &(a, b)) in boundary_edges.iter().enumerate() {
622            if !used[i] && a == current.1 {
623                used[i] = true;
624                current = (a, b);
625                found = true;
626                break;
627            }
628        }
629        if !found {
630            break;
631        }
632    }
633
634    // Remove the last vertex (it's the same as the first).
635    if boundary.len() > 1 && boundary.last() == boundary.first() {
636        boundary.pop();
637    }
638
639    boundary
640}
641
642// ───────────────────────────────────────────────────────────────────────────
643//  Tests
644// ───────────────────────────────────────────────────────────────────────────
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    fn pt(x: f64, y: f64) -> Point2 {
651        Point2::new(x, y)
652    }
653
654    // ── Basic build ─────────────────────────────────────────────────────
655
656    #[test]
657    fn single_triangle_builds() {
658        let verts = vec![pt(0.0, 0.0), pt(4.0, 0.0), pt(2.0, 4.0)];
659        let tris = vec![[0, 1, 2]];
660        let faces = vec![0];
661        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
662        assert!(h.num_levels() >= 1);
663        assert!(h.num_finest_triangles() >= 1);
664    }
665
666    #[test]
667    fn two_triangles_builds() {
668        let verts = vec![pt(0.0, 0.0), pt(2.0, 0.0), pt(4.0, 0.0), pt(2.0, 4.0)];
669        let tris = vec![[0, 1, 3], [1, 2, 3]];
670        let faces = vec![0, 1];
671        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
672        assert!(h.num_levels() >= 1);
673    }
674
675    #[test]
676    fn empty_triangulation_errors() {
677        let verts = vec![pt(0.0, 0.0), pt(1.0, 0.0), pt(0.0, 1.0)];
678        let result = KirkpatrickHierarchy::build(&verts, &[], &[]);
679        assert!(matches!(result, Err(KirkpatrickError::EmptyTriangulation)));
680    }
681
682    #[test]
683    fn too_few_vertices_errors() {
684        let verts = vec![pt(0.0, 0.0), pt(1.0, 0.0)];
685        let result = KirkpatrickHierarchy::build(&verts, &[[0, 0, 0]], &[0]);
686        assert!(matches!(result, Err(KirkpatrickError::TooFewVertices)));
687    }
688
689    // ── Point location ──────────────────────────────────────────────────
690
691    #[test]
692    fn locate_in_single_triangle() {
693        let verts = vec![pt(0.0, 0.0), pt(4.0, 0.0), pt(2.0, 4.0)];
694        let tris = vec![[0, 1, 2]];
695        let faces = vec![0];
696        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
697        let result = h.locate(pt(2.0, 1.0)).unwrap();
698        assert_eq!(result, Some(0));
699    }
700
701    #[test]
702    fn locate_in_two_triangles() {
703        let verts = vec![pt(0.0, 0.0), pt(2.0, 0.0), pt(4.0, 0.0), pt(2.0, 4.0)];
704        let tris = vec![[0, 1, 3], [1, 2, 3]];
705        let faces = vec![0, 1];
706        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
707        // Point in left triangle.
708        assert_eq!(h.locate(pt(1.0, 1.0)).unwrap(), Some(0));
709        // Point in right triangle.
710        assert_eq!(h.locate(pt(3.0, 1.0)).unwrap(), Some(1));
711    }
712
713    #[test]
714    fn locate_outside_bbox_errors() {
715        let verts = vec![pt(0.0, 0.0), pt(4.0, 0.0), pt(2.0, 4.0)];
716        let tris = vec![[0, 1, 2]];
717        let faces = vec![0];
718        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
719        assert!(matches!(
720            h.locate(pt(1000.0, 1000.0)),
721            Err(KirkpatrickError::OutsideBoundingTriangle)
722        ));
723    }
724
725    // ── Grid triangulation ──────────────────────────────────────────────
726
727    fn build_grid_triangulation(
728        nx: usize,
729        ny: usize,
730    ) -> (Vec<Point2>, Vec<[usize; 3]>, Vec<usize>) {
731        let mut verts = Vec::new();
732        for j in 0..=ny {
733            for i in 0..=nx {
734                verts.push(pt(i as f64, j as f64));
735            }
736        }
737        let idx = |i: usize, j: usize| j * (nx + 1) + i;
738
739        let mut tris = Vec::new();
740        let mut faces = Vec::new();
741        let mut fi = 0;
742        for j in 0..ny {
743            for i in 0..nx {
744                let a = idx(i, j);
745                let b = idx(i + 1, j);
746                let c = idx(i + 1, j + 1);
747                let d = idx(i, j + 1);
748                tris.push([a, b, c]);
749                faces.push(fi);
750                fi += 1;
751                tris.push([a, c, d]);
752                faces.push(fi);
753                fi += 1;
754            }
755        }
756        (verts, tris, faces)
757    }
758
759    #[test]
760    fn grid_2x2_locate_all_faces() {
761        let (verts, tris, faces) = build_grid_triangulation(2, 2);
762        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
763
764        // Check every cell center.
765        for j in 0..2 {
766            for i in 0..2 {
767                let qx = i as f64 + 0.25;
768                let qy = j as f64 + 0.25;
769                let result = h.locate(pt(qx, qy)).unwrap();
770                assert!(
771                    result.is_some(),
772                    "point ({}, {}) should be in a face",
773                    qx,
774                    qy
775                );
776            }
777        }
778    }
779
780    #[test]
781    fn grid_4x4_locate_matches_brute_force() {
782        let (verts, tris, faces) = build_grid_triangulation(4, 4);
783        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
784
785        for j in 0..4 {
786            for i in 0..4 {
787                let qx = i as f64 + 0.3;
788                let qy = j as f64 + 0.3;
789                let p = pt(qx, qy);
790                let dag = h.locate(p).unwrap();
791                let bf = h.locate_brute_force(p);
792                // Both should find a face (or both None).
793                assert_eq!(
794                    dag.is_some(),
795                    bf.is_some(),
796                    "mismatch at ({}, {}): dag={:?}, bf={:?}",
797                    qx,
798                    qy,
799                    dag,
800                    bf
801                );
802            }
803        }
804    }
805
806    #[test]
807    fn grid_4x4_locate_edge_centers() {
808        let (verts, tris, faces) = build_grid_triangulation(4, 4);
809        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
810
811        // Test points on grid lines (edges between triangles).
812        for i in 0..4 {
813            let p = pt(i as f64 + 0.5, 2.0);
814            let result = h.locate(p);
815            assert!(
816                result.is_ok(),
817                "edge point ({}, 2) should be locatable",
818                p.x
819            );
820        }
821    }
822
823    // ── Hierarchy properties ────────────────────────────────────────────
824
825    #[test]
826    fn hierarchy_coarsens() {
827        let (verts, tris, faces) = build_grid_triangulation(4, 4);
828        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
829        // The coarsest level should have fewer triangles than the finest.
830        assert!(h.num_coarsest_triangles() < h.num_finest_triangles());
831        // And at least 1 triangle.
832        assert!(h.num_coarsest_triangles() >= 1);
833        // Multiple levels.
834        assert!(h.num_levels() >= 2);
835    }
836
837    #[test]
838    fn larger_grid_builds_and_locates() {
839        let (verts, tris, faces) = build_grid_triangulation(8, 8);
840        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
841        assert!(h.num_levels() >= 2);
842
843        // Sample queries.
844        for j in 0..8 {
845            for i in 0..8 {
846                let qx = i as f64 + 0.5;
847                let qy = j as f64 + 0.5;
848                let result = h.locate(pt(qx, qy)).unwrap();
849                assert!(
850                    result.is_some(),
851                    "point ({}, {}) should be in a face",
852                    qx,
853                    qy
854                );
855            }
856        }
857    }
858
859    // ── Error display ───────────────────────────────────────────────────
860
861    #[test]
862    fn error_display() {
863        assert!(KirkpatrickError::EmptyTriangulation
864            .to_string()
865            .contains("empty"));
866        assert!(KirkpatrickError::TooFewVertices
867            .to_string()
868            .contains("at least 3"));
869        assert!(KirkpatrickError::OutsideBoundingTriangle
870            .to_string()
871            .contains("bounding triangle"));
872    }
873
874    // ── Determinism ─────────────────────────────────────────────────────
875
876    #[test]
877    fn same_input_produces_same_hierarchy() {
878        let (verts, tris, faces) = build_grid_triangulation(4, 4);
879        let h1 = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
880        let h2 = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
881
882        assert_eq!(h1.num_levels(), h2.num_levels());
883        assert_eq!(h1.num_finest_triangles(), h2.num_finest_triangles());
884
885        for j in 0..4 {
886            for i in 0..4 {
887                let p = pt(i as f64 + 0.3, j as f64 + 0.3);
888                assert_eq!(h1.locate(p), h2.locate(p), "mismatch at ({}, {})", p.x, p.y);
889            }
890        }
891    }
892
893    // ── Exterior points ─────────────────────────────────────────────────
894
895    #[test]
896    fn exterior_point_returns_none() {
897        let (verts, tris, faces) = build_grid_triangulation(2, 2);
898        let h = KirkpatrickHierarchy::build(&verts, &tris, &faces).unwrap();
899
900        // Point inside the grid but in the exterior (between grid boundary
901        // and bounding triangle). The bounding triangle is huge, so this
902        // point is inside the bbox but outside the grid.
903        let result = h.locate(pt(-0.5, -0.5));
904        // Should be Ok(None) — inside bbox but in an exterior face.
905        assert!(result.is_ok());
906        assert_eq!(result.unwrap(), None);
907    }
908}