1//! Contains CellOccupancyMatrix used to track occupied cells during grid placement
2use super::TrackCounts;
3use crate::compute::grid::OriginZeroLine;
4use crate::geometry::AbsoluteAxis;
5use crate::geometry::Line;
6use crate::util::sys::Vec;
7use core::cmp::{max, min};
8use core::fmt::Debug;
9use core::ops::Range;
10use grid::Grid;
1112/// The occupancy state of a single grid cell
13#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
14pub(crate) enum CellOccupancyState {
15#[default]
16/// Indicates that a grid cell is unoccupied
17Unoccupied,
18/// Indicates that a grid cell is occupied by a definitely placed item
19DefinitelyPlaced,
20/// Indicates that a grid cell is occupied by an item that was placed by the auto placement algorithm
21AutoPlaced,
22}
2324/// A dynamically sized matrix (2d grid) which tracks the occupancy of each grid cell during auto-placement
25/// It also keeps tabs on how many tracks there are and which tracks are implicit and which are explicit.
26pub(crate) struct CellOccupancyMatrix {
27/// The grid of occupancy states
28inner: Grid<CellOccupancyState>,
29/// The counts of implicit and explicit columns
30columns: TrackCounts,
31/// The counts of implicit and explicit rows
32rows: TrackCounts,
33}
3435/// Debug impl that represents the matrix in a compact 2d text format
36impl Debug for CellOccupancyMatrix {
37fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38writeln!(
39 f,
40"Rows: neg_implicit={} explicit={} pos_implicit={}",
41self.rows.negative_implicit, self.rows.explicit, self.rows.positive_implicit
42 )?;
43writeln!(
44 f,
45"Cols: neg_implicit={} explicit={} pos_implicit={}",
46self.columns.negative_implicit, self.columns.explicit, self.columns.positive_implicit
47 )?;
48writeln!(f, "State:")?;
4950for row_idx in 0..self.inner.rows() {
51for cell in self.inner.iter_row(row_idx) {
52let letter = match *cell {
53 CellOccupancyState::Unoccupied => '_',
54 CellOccupancyState::DefinitelyPlaced => 'D',
55 CellOccupancyState::AutoPlaced => 'A',
56 };
57write!(f, "{letter}")?;
58 }
59writeln!(f)?;
60 }
6162Ok(())
63 }
64}
6566impl CellOccupancyMatrix {
67/// Create a CellOccupancyMatrix given a set of provisional track counts. The grid can expand as needed to fit more tracks,
68 /// the provisional track counts represent a best effort attempt to avoid the extra allocations this requires.
69pub fn with_track_counts(columns: TrackCounts, rows: TrackCounts) -> Self {
70Self { inner: Grid::new(rows.len(), columns.len()), rows, columns }
71 }
7273/// Determines whether the specified area fits within the tracks currently represented by the matrix
74pub fn is_area_in_range(
75&self,
76 primary_axis: AbsoluteAxis,
77 primary_range: Range<i16>,
78 secondary_range: Range<i16>,
79 ) -> bool {
80if primary_range.start < 0 || primary_range.end > self.track_counts(primary_axis).len() as i16 {
81return false;
82 }
83if secondary_range.start < 0 || secondary_range.end > self.track_counts(primary_axis.other_axis()).len() as i16
84 {
85return false;
86 }
87true
88}
8990/// Expands the grid (potentially in all 4 directions) in order to ensure that the specified range fits within the allocated space
91fn expand_to_fit_range(&mut self, row_range: Range<i16>, col_range: Range<i16>) {
92// Calculate number of rows and columns missing to accomodate ranges (if any)
93let req_negative_rows = min(row_range.start, 0);
94let req_positive_rows = max(row_range.end - self.rows.len() as i16, 0);
95let req_negative_cols = min(col_range.start, 0);
96let req_positive_cols = max(col_range.end - self.columns.len() as i16, 0);
9798let old_row_count = self.rows.len();
99let old_col_count = self.columns.len();
100let new_row_count = old_row_count + (req_negative_rows + req_positive_rows) as usize;
101let new_col_count = old_col_count + (req_negative_cols + req_positive_cols) as usize;
102103let mut data = Vec::with_capacity(new_row_count * new_col_count);
104105// Push new negative rows
106for _ in 0..(req_negative_rows as usize * new_col_count) {
107 data.push(CellOccupancyState::Unoccupied);
108 }
109110// Push existing rows
111for row in 0..old_row_count {
112// Push new negative columns
113for _ in 0..req_negative_cols {
114 data.push(CellOccupancyState::Unoccupied);
115 }
116// Push existing columns
117for col in 0..old_col_count {
118 data.push(*self.inner.get(row, col).unwrap());
119 }
120// Push new positive columns
121for _ in 0..req_positive_cols {
122 data.push(CellOccupancyState::Unoccupied);
123 }
124 }
125126// Push new negative rows
127for _ in 0..(req_positive_rows as usize * new_col_count) {
128 data.push(CellOccupancyState::Unoccupied);
129 }
130131// Update self with new data
132self.inner = Grid::from_vec(data, new_col_count);
133self.rows.negative_implicit += req_negative_rows as u16;
134self.rows.positive_implicit += req_positive_rows as u16;
135self.columns.negative_implicit += req_negative_cols as u16;
136self.columns.positive_implicit += req_positive_cols as u16;
137 }
138139/// Mark an area of the matrix as occupied, expanding the allocated space as necessary to accomodate the passed area.
140pub fn mark_area_as(
141&mut self,
142 primary_axis: AbsoluteAxis,
143 primary_span: Line<OriginZeroLine>,
144 secondary_span: Line<OriginZeroLine>,
145 value: CellOccupancyState,
146 ) {
147let (row_span, column_span) = match primary_axis {
148 AbsoluteAxis::Horizontal => (secondary_span, primary_span),
149 AbsoluteAxis::Vertical => (primary_span, secondary_span),
150 };
151152let mut col_range = self.columns.oz_line_range_to_track_range(column_span);
153let mut row_range = self.rows.oz_line_range_to_track_range(row_span);
154155// Check that if the resolved ranges fit within the allocated grid. And if they don't then expand the grid to fit
156 // and then re-resolve the ranges once the grid has been expanded as the resolved indexes may have changed
157let is_in_range = self.is_area_in_range(AbsoluteAxis::Horizontal, col_range.clone(), row_range.clone());
158if !is_in_range {
159self.expand_to_fit_range(row_range.clone(), col_range.clone());
160 col_range = self.columns.oz_line_range_to_track_range(column_span);
161 row_range = self.rows.oz_line_range_to_track_range(row_span);
162 }
163164for x in row_range {
165for y in col_range.clone() {
166*self.inner.get_mut(x as usize, y as usize).unwrap() = value;
167 }
168 }
169 }
170171/// Determines whether a grid area specified by the bounding grid lines in OriginZero coordinates
172 /// is entirely unnocupied. Returns true if all grid cells within the grid area are unnocupied, else false.
173pub fn line_area_is_unoccupied(
174&self,
175 primary_axis: AbsoluteAxis,
176 primary_span: Line<OriginZeroLine>,
177 secondary_span: Line<OriginZeroLine>,
178 ) -> bool {
179let primary_range = self.track_counts(primary_axis).oz_line_range_to_track_range(primary_span);
180let secondary_range = self.track_counts(primary_axis.other_axis()).oz_line_range_to_track_range(secondary_span);
181self.track_area_is_unoccupied(primary_axis, primary_range, secondary_range)
182 }
183184/// Determines whether a grid area specified by a range of indexes into this CellOccupancyMatrix
185 /// is entirely unnocupied. Returns true if all grid cells within the grid area are unnocupied, else false.
186pub fn track_area_is_unoccupied(
187&self,
188 primary_axis: AbsoluteAxis,
189 primary_range: Range<i16>,
190 secondary_range: Range<i16>,
191 ) -> bool {
192let (row_range, col_range) = match primary_axis {
193 AbsoluteAxis::Horizontal => (secondary_range, primary_range),
194 AbsoluteAxis::Vertical => (primary_range, secondary_range),
195 };
196197// Search for occupied cells in the specified area. Out of bounds cells are considered unoccupied.
198for x in row_range {
199for y in col_range.clone() {
200match self.inner.get(x as usize, y as usize) {
201None | Some(CellOccupancyState::Unoccupied) => continue,
202_ => return false,
203 }
204 }
205 }
206207true
208}
209210/// Determines whether the specified row contains any items
211pub fn row_is_occupied(&self, row_index: usize) -> bool {
212self.inner.iter_row(row_index).any(|cell| !matches!(cell, CellOccupancyState::Unoccupied))
213 }
214215/// Determines whether the specified column contains any items
216pub fn column_is_occupied(&self, column_index: usize) -> bool {
217self.inner.iter_col(column_index).any(|cell| !matches!(cell, CellOccupancyState::Unoccupied))
218 }
219220/// Returns the track counts of this CellOccunpancyMatrix in the relevant axis
221pub fn track_counts(&self, track_type: AbsoluteAxis) -> &TrackCounts {
222match track_type {
223 AbsoluteAxis::Horizontal => &self.columns,
224 AbsoluteAxis::Vertical => &self.rows,
225 }
226 }
227228/// Given an axis and a track index
229 /// Search backwards from the end of the track and find the last grid cell matching the specified state (if any)
230 /// Return the index of that cell or None.
231pub fn last_of_type(
232&self,
233 track_type: AbsoluteAxis,
234 start_at: OriginZeroLine,
235 kind: CellOccupancyState,
236 ) -> Option<OriginZeroLine> {
237let track_counts = self.track_counts(track_type.other_axis());
238let track_computed_index = track_counts.oz_line_to_next_track(start_at);
239240let maybe_index = match track_type {
241 AbsoluteAxis::Horizontal => {
242self.inner.iter_row(track_computed_index as usize).rposition(|item| *item == kind)
243 }
244 AbsoluteAxis::Vertical => {
245self.inner.iter_col(track_computed_index as usize).rposition(|item| *item == kind)
246 }
247 };
248249 maybe_index.map(|idx| track_counts.track_to_prev_oz_line(idx as u16))
250 }
251}