geometry_trait/indexed_access.rs
1//! Two-axis indexed access for geometries that are addressed by an
2//! `(index, dim)` pair rather than `dim` alone (Box, Segment).
3//!
4//! Mirrors `boost::geometry::traits::indexed_access<G, Index, Dimension>`
5//! from `boost/geometry/core/access.hpp:79-84`, together with the
6//! `min_corner` / `max_corner` constants declared in the same header
7//! at `boost/geometry/core/access.hpp:36-39`.
8//!
9//! Design notes:
10//! * Both axes — the corner index `I` and the dimension `D` — are
11//! const generics, matching Boost's `template <std::size_t Index,
12//! std::size_t Dimension>` on `traits::indexed_access`. Out-of-range
13//! `I` or `D` is rejected at the call site rather than at runtime
14//! (see `tests/ui/indexed_out_of_range.rs`).
15//! * The Boost C++ side specialises `traits::indexed_access` separately
16//! per `(Geometry, Index, Dimension)` triple. On the Rust side a
17//! single trait with two const generics replaces all of those
18//! specialisations.
19
20use crate::geometry::Geometry;
21use crate::point::Point;
22
23/// Two-axis (`Index`, `Dimension`) access for geometries built out of
24/// a pair of corner points — Box and Segment.
25///
26/// Mirrors `boost::geometry::traits::indexed_access<G, Index, Dimension>`
27/// from `boost/geometry/core/access.hpp:79-84`. The associated
28/// scalar type is projected through the geometry's `Point`, matching
29/// `boost::geometry::coordinate_type<G>::type` for indexed geometries.
30///
31/// For a Box, `I` is one of [`corner::MIN`] / [`corner::MAX`]; for a
32/// Segment, `I` is the endpoint index `0` / `1`. `D` is the
33/// coordinate dimension, with the same `0 <= D < Point::DIM`
34/// invariant as [`Point::get`].
35///
36/// # Examples
37///
38/// ```
39/// use geometry_trait::{corner, IndexedAccess};
40///
41/// // Generic helpers can require `IndexedAccess` and use the corner
42/// // constants — same Box and Segment surface.
43/// fn x_min<G>(g: &G) -> <G::Point as geometry_trait::Point>::Scalar
44/// where G: IndexedAccess { g.get_indexed::<{ corner::MIN }, 0>() }
45/// ```
46pub trait IndexedAccess: Geometry {
47 /// Read coordinate `D` of corner `I`.
48 ///
49 /// Mirrors `boost::geometry::traits::indexed_access<G, I, D>::get(g)`
50 /// (`boost/geometry/core/access.hpp:79-84`). Both `I` and `D` are
51 /// const generics: passing an out-of-range value is rejected at
52 /// the call site rather than at runtime.
53 fn get_indexed<const I: usize, const D: usize>(&self) -> <Self::Point as Point>::Scalar;
54
55 /// Write coordinate `D` of corner `I`.
56 ///
57 /// Mirrors `boost::geometry::traits::indexed_access<G, I, D>::set(g, v)`
58 /// (`boost/geometry/core/access.hpp:79-84`).
59 fn set_indexed<const I: usize, const D: usize>(
60 &mut self,
61 value: <Self::Point as Point>::Scalar,
62 );
63}
64
65/// Indices used when addressing the two corners of a Box.
66///
67/// # Examples
68///
69/// ```
70/// use geometry_trait::corner;
71/// assert_eq!(corner::MIN, 0);
72/// assert_eq!(corner::MAX, 1);
73/// ```
74///
75/// Mirrors `boost::geometry::min_corner` and `boost::geometry::max_corner`
76/// declared at `boost/geometry/core/access.hpp:36-39`. For Segment,
77/// the same `0` / `1` integers select the first / second endpoint;
78/// segment endpoints are not "min" or "max", so call sites that read
79/// segments should write the literal `0` / `1` rather than these
80/// constants — matching the Boost convention where `min_corner` /
81/// `max_corner` are box-specific names even though the underlying
82/// integers are shared.
83pub mod corner {
84 /// Index of the minimum corner of a Box.
85 ///
86 /// Mirrors `boost::geometry::min_corner`
87 /// (`boost/geometry/core/access.hpp:36`).
88 pub const MIN: usize = 0;
89
90 /// Index of the maximum corner of a Box.
91 ///
92 /// Mirrors `boost::geometry::max_corner`
93 /// (`boost/geometry/core/access.hpp:39`).
94 pub const MAX: usize = 1;
95}
96
97#[cfg(test)]
98mod tests {
99 //! Smoke test mirroring `boost/geometry/test/core/access.cpp:55-86`
100 //! (`test_indexed_get_set` / `test_indexed_get`): set all four
101 //! `(I, D)` slots of a 2D Box-like value, read them back, assert
102 //! the values round-trip.
103
104 use super::{IndexedAccess, corner};
105 use crate::geometry::Geometry;
106 use crate::point::Point;
107 use geometry_cs::Cartesian;
108 use geometry_tag::{BoxTag, PointTag};
109
110 struct P2(f64, f64);
111
112 impl Geometry for P2 {
113 type Kind = PointTag;
114 type Point = Self;
115 }
116
117 impl Point for P2 {
118 type Scalar = f64;
119 type Cs = Cartesian;
120 const DIM: usize = 2;
121
122 fn get<const D: usize>(&self) -> f64 {
123 const {
124 assert!(D < <P2 as Point>::DIM, "Point::get: dimension out of range");
125 }
126 if D == 0 { self.0 } else { self.1 }
127 }
128 }
129
130 struct B {
131 corners: [[f64; 2]; 2],
132 }
133
134 impl Geometry for B {
135 type Kind = BoxTag;
136 type Point = P2;
137 }
138
139 impl IndexedAccess for B {
140 fn get_indexed<const I: usize, const D: usize>(&self) -> f64 {
141 const {
142 assert!(I < 2, "IndexedAccess::get_indexed: index out of range");
143 assert!(D < 2, "IndexedAccess::get_indexed: dimension out of range");
144 }
145 self.corners[I][D]
146 }
147
148 fn set_indexed<const I: usize, const D: usize>(&mut self, v: f64) {
149 const {
150 assert!(I < 2, "IndexedAccess::set_indexed: index out of range");
151 assert!(D < 2, "IndexedAccess::set_indexed: dimension out of range");
152 }
153 self.corners[I][D] = v;
154 }
155 }
156
157 #[test]
158 fn indexed_get_set_roundtrip() {
159 let point = P2(5.0, 6.0);
160 assert_eq!(point.get::<0>().to_bits(), 5.0_f64.to_bits());
161 assert_eq!(point.get::<1>().to_bits(), 6.0_f64.to_bits());
162
163 let mut b = B {
164 corners: [[0.0; 2]; 2],
165 };
166 b.set_indexed::<{ corner::MIN }, 0>(1.0);
167 b.set_indexed::<{ corner::MIN }, 1>(2.0);
168 b.set_indexed::<{ corner::MAX }, 0>(3.0);
169 b.set_indexed::<{ corner::MAX }, 1>(4.0);
170
171 // Bit-equal comparison: each slot was written with a literal
172 // f64 constant and read back without arithmetic, so the round
173 // trip must be exact. Using `to_bits()` sidesteps the
174 // `clippy::float_cmp` lint without weakening the assertion.
175 assert_eq!(
176 b.get_indexed::<{ corner::MIN }, 0>().to_bits(),
177 1.0_f64.to_bits()
178 );
179 assert_eq!(
180 b.get_indexed::<{ corner::MIN }, 1>().to_bits(),
181 2.0_f64.to_bits()
182 );
183 assert_eq!(
184 b.get_indexed::<{ corner::MAX }, 0>().to_bits(),
185 3.0_f64.to_bits()
186 );
187 assert_eq!(
188 b.get_indexed::<{ corner::MAX }, 1>().to_bits(),
189 4.0_f64.to_bits()
190 );
191 }
192
193 #[test]
194 fn corner_constants_match_boost() {
195 // `boost/geometry/core/access.hpp:36-39` pins MIN = 0, MAX = 1.
196 assert_eq!(corner::MIN, 0);
197 assert_eq!(corner::MAX, 1);
198 }
199}