Skip to main content

henad_core/authoring/model/
field.rs

1//! The grid slot an agent model sits over, and the [`Extent`] of the world.
2//!
3//! A field layer owns cells, updates them once per tick and draws them.
4
5use crate::params::{ParamDescriptor, ParamValue};
6use crate::view::GridView;
7
8/// The world rectangle every display layer stretches to.
9///
10/// The whole model shares one extent. An agent layer and a field layer then cannot disagree about
11/// how big the world is.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Extent {
14    /// Width in world units.
15    pub w: f32,
16    /// Height in world units.
17    pub h: f32,
18}
19
20impl Extent {
21    /// Returns the cell dimensions of a field that tiles this extent at one cell per unit.
22    pub fn cells(self) -> (u32, u32) {
23        (self.w.max(1.0) as u32, self.h.max(1.0) as u32)
24    }
25}
26
27/// A layer of cells stepped once per tick.
28///
29/// henad-compute implements it for `CaField`, a [`crate::authoring::model::grid_model::GridModel`] as a layer, and
30/// for `ScalarField`, scatter-plus-decay layers. An agent model sits over either layer, or over [`NoField`].
31pub trait FieldLayer: Send + 'static {
32    /// Whether the layer draws a grid. It must agree with [`Self::grid_view`].
33    const HAS_GRID: bool = true;
34
35    /// Name of this layer in the Model panel.
36    const KIND: &'static str;
37
38    /// Hot parameters, rebuilt once per tick.
39    type Params: Send + Sync;
40    /// The field as an agent kernel sees it.
41    type Read<'a>
42    where
43        Self: 'a;
44    /// Per agent deposit lanes, filled by the agent passes, or `()` for a field without deposit lanes.
45    type DepositLanes: Send + 'static;
46
47    /// Returns this layer's own parameters, listed after the model's parameters.
48    fn param_descriptors() -> Vec<ParamDescriptor>;
49    /// Extracts the hot parameters for one tick.
50    ///
51    /// `params` is this layer's own slice, so its indices are 0 based and do not move when the
52    /// model above it gains a parameter.
53    fn from_params(params: &[ParamValue]) -> Self::Params;
54    /// Creates the layer over `extent`, from this layer's own slice of the parameters.
55    fn new(extent: Extent, params: &[ParamValue]) -> Self;
56
57    /// Returns the field as an agent kernel reads it.
58    fn read(&self) -> Self::Read<'_>;
59    /// Allocates deposit lanes for `n` agents. The engine reuses them every tick.
60    fn alloc_deposits(&self, n: usize) -> Self::DepositLanes;
61    /// Advances the layer by one tick, with this tick's deposits.
62    fn update(&mut self, deposits: &Self::DepositLanes, p: &Self::Params, tick: u64);
63
64    /// Turns cells into palette indices. The engine calls it before each snapshot.
65    fn prepare_view(&mut self) {}
66
67    /// Returns the cells to draw, or `None` for a layer without a grid.
68    fn grid_view(&self) -> Option<GridView<'_>>;
69    /// Number of cells.
70    fn cell_count(&self) -> usize;
71    /// Heap memory held by the layer, in bytes.
72    fn heap_bytes(&self) -> usize;
73}
74
75/// The empty grid slot, for a model that is agents only.
76#[derive(Debug)]
77pub struct NoField;
78
79impl FieldLayer for NoField {
80    const HAS_GRID: bool = false;
81    const KIND: &'static str = "None";
82
83    type Params = ();
84    type Read<'a> = ();
85    type DepositLanes = ();
86
87    fn param_descriptors() -> Vec<ParamDescriptor> {
88        Vec::new()
89    }
90
91    fn from_params(_params: &[ParamValue]) {}
92
93    fn new(_extent: Extent, _params: &[ParamValue]) -> Self {
94        Self
95    }
96
97    fn read(&self) {}
98
99    fn alloc_deposits(&self, _n: usize) {}
100
101    fn update(&mut self, _deposits: &(), _p: &(), _tick: u64) {}
102
103    fn grid_view(&self) -> Option<GridView<'_>> {
104        None
105    }
106
107    fn cell_count(&self) -> usize {
108        0
109    }
110
111    fn heap_bytes(&self) -> usize {
112        0
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn extent_rounds_down_to_whole_cells() {
122        assert_eq!(Extent { w: 200.0, h: 200.0 }.cells(), (200, 200));
123        assert_eq!(Extent { w: 10.9, h: 4.2 }.cells(), (10, 4));
124    }
125
126    /// A zero extent would give a field with no cells and divide-by-zero indexing.
127    #[test]
128    fn extent_never_collapses_below_one_cell() {
129        assert_eq!(Extent { w: 0.0, h: 0.0 }.cells(), (1, 1));
130        assert_eq!(Extent { w: -5.0, h: 0.4 }.cells(), (1, 1));
131    }
132}