axiolid_field/
field.rs

1//! Compact row-major grid of layered cells plus its sampling evidence.
2
3use axiolid_core::Tolerance;
4
5use crate::{FieldConfig, LayeredCell, LayeredFieldError};
6
7/// Structured facts about one sampling run.
8///
9/// Evidence is reported, never used to silently alter the result.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11#[non_exhaustive]
12pub struct FieldEvidence {
13    /// Cells visited by the sampler.
14    pub cells_sampled: usize,
15    /// Surface crossings stored across all cells.
16    pub surface_hits: usize,
17    /// Occupancy spans stored across all cells.
18    pub occupancy_spans: usize,
19    /// Cells holding no layer at all.
20    pub empty_cells: usize,
21    /// Cells holding more than one surface crossing.
22    pub multi_layer_cells: usize,
23    /// Triangles skipped because they are parallel to the sampling direction.
24    pub parallel_triangles_skipped: usize,
25    /// Crossings discarded because they fell outside the local `w` bounds.
26    pub out_of_bounds_hits: usize,
27    /// Crossings that landed on a triangle edge or vertex within tolerance.
28    pub boundary_contacts: usize,
29    /// Coincident crossings collapsed because adjacent facets share an edge.
30    ///
31    /// A sampling line through a shared edge meets both facets, but the two
32    /// reports describe one surface. They are merged and counted here rather
33    /// than stored twice or dropped silently.
34    pub coincident_hits_merged: usize,
35    /// Triangles rejected as degenerate before sampling.
36    pub degenerate_triangles: usize,
37}
38
39/// Deterministic row-major field of layered cells.
40#[derive(Debug, Clone, PartialEq)]
41pub struct LayeredField {
42    width: usize,
43    height: usize,
44    cells: Vec<LayeredCell>,
45    evidence: FieldEvidence,
46}
47
48impl LayeredField {
49    /// Allocate an empty field from a validated configuration.
50    pub fn with_config(config: &FieldConfig) -> Result<Self, LayeredFieldError> {
51        let (width, height) = config.dimensions();
52        Self::empty(width, height)
53    }
54
55    /// Allocate an empty field with explicit dimensions.
56    pub fn empty(width: usize, height: usize) -> Result<Self, LayeredFieldError> {
57        let cell_count = width
58            .checked_mul(height)
59            .ok_or(LayeredFieldError::InvalidDimensions)?;
60        if cell_count == 0 {
61            return Err(LayeredFieldError::InvalidDimensions);
62        }
63        Ok(Self {
64            width,
65            height,
66            cells: vec![LayeredCell::empty(); cell_count],
67            evidence: FieldEvidence::default(),
68        })
69    }
70
71    /// Cell dimensions.
72    pub const fn dimensions(&self) -> (usize, usize) {
73        (self.width, self.height)
74    }
75
76    /// Number of cells in row-major storage.
77    pub fn cell_count(&self) -> usize {
78        self.cells.len()
79    }
80
81    /// Row-major address for `(x, y)`, or `None` when outside the field.
82    pub fn linear_index(&self, x: usize, y: usize) -> Option<usize> {
83        (x < self.width && y < self.height).then_some(y * self.width + x)
84    }
85
86    /// Borrow the cell at `(x, y)`.
87    pub fn cell(&self, x: usize, y: usize) -> Option<&LayeredCell> {
88        self.linear_index(x, y).map(|index| &self.cells[index])
89    }
90
91    /// Cells in row-major order.
92    pub fn cells(&self) -> &[LayeredCell] {
93        &self.cells
94    }
95
96    /// Structured sampling facts.
97    pub const fn evidence(&self) -> FieldEvidence {
98        self.evidence
99    }
100
101    /// Build a field from a complete row-major cell buffer and sampling evidence.
102    ///
103    /// The constructor rejects empty, overflowing, or length-mismatched storage;
104    /// callers cannot construct a partially initialized field.
105    pub fn from_cells(
106        width: usize,
107        height: usize,
108        cells: Vec<LayeredCell>,
109        evidence: FieldEvidence,
110    ) -> Result<Self, LayeredFieldError> {
111        let expected = width
112            .checked_mul(height)
113            .ok_or(LayeredFieldError::InvalidDimensions)?;
114        if expected == 0 || cells.len() != expected {
115            return Err(LayeredFieldError::DimensionMismatch);
116        }
117        Ok(Self {
118            width,
119            height,
120            cells,
121            evidence,
122        })
123    }
124
125    /// Derive occupancy spans in every cell from its crossing sequence.
126    ///
127    /// Requires closed, consistently wound input: an unbalanced column is
128    /// reported rather than repaired.
129    pub fn derive_occupancy(&self, tolerance: Tolerance) -> Result<Self, LayeredFieldError> {
130        let mut derived = self.clone();
131        let mut evidence = self.evidence;
132        evidence.occupancy_spans = 0;
133        for (index, cell) in self.cells.iter().enumerate() {
134            let updated = cell.derive_occupancy(tolerance)?;
135            evidence.occupancy_spans += updated.occupancy().len();
136            derived.cells[index] = updated;
137        }
138        derived.evidence = evidence;
139        Ok(derived)
140    }
141}