Skip to main content

ph_surfaces/
boundary.rs

1//! Boundary policy vocabulary for the declared domain of a surface.
2//!
3//! A surface declares a rectangular domain: the closed interval spanned by its
4//! X axis crossed with the closed interval spanned by its Y axis. Each of the
5//! four sides of that domain independently selects what happens to a
6//! coordinate falling outside it.
7
8/// Behaviour selected for one side of one axis.
9///
10/// This is the whole v0.1 vocabulary. Extrapolation is never performed under
11/// either selection.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum Boundary {
14    /// Reject the coordinate and report the applicable bound.
15    ///
16    /// This is the default for every side.
17    #[default]
18    Error,
19    /// Substitute the nearest declared endpoint coordinate.
20    ///
21    /// The substituted coordinate is a declared knot, so the result is always
22    /// inside the convex hull of the stored values.
23    Clamp,
24}
25
26/// The four independent [`Boundary`] selections of one surface.
27///
28/// Every side defaults to [`Boundary::Error`], so a surface rejects
29/// out-of-domain coordinates unless the definition opts into clamping. The
30/// four sides are named rather than positional: there is no tuple of booleans
31/// to memorise the order of.
32///
33/// # Examples
34///
35/// ```
36/// use ph_surfaces::{Boundary, BoundaryPolicy};
37///
38/// const POLICY: BoundaryPolicy = BoundaryPolicy::new()
39///     .with_x_below(Boundary::Clamp)
40///     .with_y_above(Boundary::Clamp);
41///
42/// assert_eq!(POLICY.x_below(), Boundary::Clamp);
43/// assert_eq!(POLICY.x_above(), Boundary::Error);
44/// assert_eq!(POLICY.y_below(), Boundary::Error);
45/// assert_eq!(POLICY.y_above(), Boundary::Clamp);
46/// ```
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
48pub struct BoundaryPolicy {
49    x_below: Boundary,
50    x_above: Boundary,
51    y_below: Boundary,
52    y_above: Boundary,
53}
54
55impl BoundaryPolicy {
56    /// Returns the policy that rejects every out-of-domain coordinate.
57    ///
58    /// This is the same value as [`BoundaryPolicy::default`], but it is usable
59    /// in a constant or static definition.
60    #[must_use]
61    pub const fn new() -> Self {
62        Self {
63            x_below: Boundary::Error,
64            x_above: Boundary::Error,
65            y_below: Boundary::Error,
66            y_above: Boundary::Error,
67        }
68    }
69
70    /// Returns this policy with the selection for X below the domain replaced.
71    #[must_use]
72    pub const fn with_x_below(self, boundary: Boundary) -> Self {
73        Self {
74            x_below: boundary,
75            ..self
76        }
77    }
78
79    /// Returns this policy with the selection for X above the domain replaced.
80    #[must_use]
81    pub const fn with_x_above(self, boundary: Boundary) -> Self {
82        Self {
83            x_above: boundary,
84            ..self
85        }
86    }
87
88    /// Returns this policy with the selection for Y below the domain replaced.
89    #[must_use]
90    pub const fn with_y_below(self, boundary: Boundary) -> Self {
91        Self {
92            y_below: boundary,
93            ..self
94        }
95    }
96
97    /// Returns this policy with the selection for Y above the domain replaced.
98    #[must_use]
99    pub const fn with_y_above(self, boundary: Boundary) -> Self {
100        Self {
101            y_above: boundary,
102            ..self
103        }
104    }
105
106    /// Returns the selection for an X coordinate below the first X knot.
107    #[must_use]
108    pub const fn x_below(&self) -> Boundary {
109        self.x_below
110    }
111
112    /// Returns the selection for an X coordinate above the last X knot.
113    #[must_use]
114    pub const fn x_above(&self) -> Boundary {
115        self.x_above
116    }
117
118    /// Returns the selection for a Y coordinate below the first Y knot.
119    #[must_use]
120    pub const fn y_below(&self) -> Boundary {
121        self.y_below
122    }
123
124    /// Returns the selection for a Y coordinate above the last Y knot.
125    #[must_use]
126    pub const fn y_above(&self) -> Boundary {
127        self.y_above
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::{Boundary, BoundaryPolicy};
134    use core::mem::size_of;
135
136    #[test]
137    fn default_policy_rejects_every_side() {
138        let policy = BoundaryPolicy::default();
139
140        assert_eq!(policy.x_below(), Boundary::Error);
141        assert_eq!(policy.x_above(), Boundary::Error);
142        assert_eq!(policy.y_below(), Boundary::Error);
143        assert_eq!(policy.y_above(), Boundary::Error);
144    }
145
146    #[test]
147    fn default_trait_matches_the_const_constructor() {
148        assert_eq!(BoundaryPolicy::default(), BoundaryPolicy::new());
149        assert_eq!(Boundary::default(), Boundary::Error);
150    }
151
152    #[test]
153    fn each_builder_method_changes_only_its_own_side() {
154        let base = BoundaryPolicy::new();
155
156        let x_below = base.with_x_below(Boundary::Clamp);
157        assert_eq!(x_below.x_below(), Boundary::Clamp);
158        assert_eq!(x_below.x_above(), Boundary::Error);
159        assert_eq!(x_below.y_below(), Boundary::Error);
160        assert_eq!(x_below.y_above(), Boundary::Error);
161
162        let x_above = base.with_x_above(Boundary::Clamp);
163        assert_eq!(x_above.x_below(), Boundary::Error);
164        assert_eq!(x_above.x_above(), Boundary::Clamp);
165        assert_eq!(x_above.y_below(), Boundary::Error);
166        assert_eq!(x_above.y_above(), Boundary::Error);
167
168        let y_below = base.with_y_below(Boundary::Clamp);
169        assert_eq!(y_below.x_below(), Boundary::Error);
170        assert_eq!(y_below.x_above(), Boundary::Error);
171        assert_eq!(y_below.y_below(), Boundary::Clamp);
172        assert_eq!(y_below.y_above(), Boundary::Error);
173
174        let y_above = base.with_y_above(Boundary::Clamp);
175        assert_eq!(y_above.x_below(), Boundary::Error);
176        assert_eq!(y_above.x_above(), Boundary::Error);
177        assert_eq!(y_above.y_below(), Boundary::Error);
178        assert_eq!(y_above.y_above(), Boundary::Clamp);
179    }
180
181    #[test]
182    fn builders_are_usable_in_const_context() {
183        const POLICY: BoundaryPolicy = BoundaryPolicy::new()
184            .with_x_below(Boundary::Clamp)
185            .with_x_above(Boundary::Clamp)
186            .with_y_below(Boundary::Clamp)
187            .with_y_above(Boundary::Clamp);
188
189        assert_eq!(POLICY.x_below(), Boundary::Clamp);
190        assert_eq!(POLICY.x_above(), Boundary::Clamp);
191        assert_eq!(POLICY.y_below(), Boundary::Clamp);
192        assert_eq!(POLICY.y_above(), Boundary::Clamp);
193    }
194
195    #[test]
196    fn the_policy_is_four_single_byte_selections() {
197        assert_eq!(size_of::<Boundary>(), 1);
198        assert_eq!(size_of::<BoundaryPolicy>(), 4);
199    }
200}