axiolid_field_ops/
clearance.rs

1//! Neutral clearance queries along the field's local `z` axis.
2//!
3//! Every function reports a distance or a blocking fact. None of them decides
4//! whether a distance is adequate: "0.9 m of free span" is geometry, "too low"
5//! is policy and belongs to the caller.
6
7use axiolid_core::{Interval, Scalar};
8
9use crate::{FieldConfig, LayeredField, LayeredFieldError};
10
11/// Free span found above a reference layer coordinate.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct ClearanceReport {
14    /// Distance from the reference coordinate to the next blocking layer.
15    pub distance: Scalar,
16    /// Layer coordinate of the blocking crossing, if the span is bounded.
17    pub blocked_at: Option<Scalar>,
18    /// Whether the span ends at the field bound rather than at geometry.
19    pub bounded_by_field: bool,
20}
21
22/// Distance from `w` upward (along local `+z`) to the next surface crossing.
23///
24/// Crossings within `tolerance.linear()` of `w` are treated as the reference
25/// surface itself, not as a blocker, so standing on a slab does not report zero
26/// clearance against that same slab.
27pub fn clearance_above(
28    field: &LayeredField,
29    config: &FieldConfig,
30    x: usize,
31    y: usize,
32    w: Scalar,
33) -> Result<ClearanceReport, LayeredFieldError> {
34    directional_clearance(field, config, x, y, w, true)
35}
36
37/// Distance from `w` downward (against local `+z`) to the next surface crossing.
38pub fn clearance_below(
39    field: &LayeredField,
40    config: &FieldConfig,
41    x: usize,
42    y: usize,
43    w: Scalar,
44) -> Result<ClearanceReport, LayeredFieldError> {
45    directional_clearance(field, config, x, y, w, false)
46}
47
48/// Largest unoccupied span in a cell within an explicit search window.
49pub fn largest_free_span(
50    field: &LayeredField,
51    x: usize,
52    y: usize,
53    search: Interval,
54) -> Result<Option<Interval>, LayeredFieldError> {
55    let cell = field
56        .cell(x, y)
57        .ok_or(LayeredFieldError::NodeOutsideField)?;
58    Ok(cell.largest_free_span(search))
59}
60
61fn directional_clearance(
62    field: &LayeredField,
63    config: &FieldConfig,
64    x: usize,
65    y: usize,
66    w: Scalar,
67    upward: bool,
68) -> Result<ClearanceReport, LayeredFieldError> {
69    if !w.is_finite() {
70        return Err(LayeredFieldError::InvalidInterval);
71    }
72    let cell = field
73        .cell(x, y)
74        .ok_or(LayeredFieldError::NodeOutsideField)?;
75    let linear = config.tolerance().linear();
76    let span = config.bounds().normal_span();
77    let bound = if upward { span.end } else { span.start };
78
79    let blocker = cell
80        .surfaces()
81        .iter()
82        .map(|hit| hit.w())
83        .filter(|value| {
84            if upward {
85                *value > w + linear
86            } else {
87                *value < w - linear
88            }
89        })
90        .fold(None::<Scalar>, |best, value| match best {
91            Some(current) if (current - w).abs() <= (value - w).abs() => Some(current),
92            _ => Some(value),
93        });
94
95    Ok(match blocker {
96        Some(value) => ClearanceReport {
97            distance: (value - w).abs(),
98            blocked_at: Some(value),
99            bounded_by_field: false,
100        },
101        None => ClearanceReport {
102            distance: (bound - w).abs(),
103            blocked_at: None,
104            bounded_by_field: true,
105        },
106    })
107}