1use super::TrackCounts;
3use crate::compute::grid::OriginZeroLine;
4use crate::geometry::AbsoluteAxis;
5use crate::geometry::Line;
6use crate::util::sys::{new_vec_with_capacity, Vec};
7use core::cmp::{max, min};
8use core::fmt::Debug;
9use core::ops::Range;
10use smallvec::SmallVec;
11
12#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
14pub(crate) enum CellOccupancyState {
15 #[default]
16 Unoccupied,
18 DefinitelyPlaced,
20 AutoPlaced,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
27struct OccupiedInterval {
28 range: Range<i16>,
30 state: CellOccupancyState,
32}
33
34impl OccupiedInterval {
35 fn overlaps(&self, range: &Range<i16>) -> bool {
37 self.range.start < range.end && self.range.end > range.start
38 }
39}
40
41#[derive(Debug, Clone, Default)]
45struct TrackIntervals {
46 intervals: SmallVec<[OccupiedInterval; 2]>,
48 auto_placement_cursor: Option<OriginZeroLine>,
50}
51
52impl TrackIntervals {
53 fn is_empty(&self) -> bool {
55 self.intervals.is_empty()
56 }
57
58 fn state_at(&self, coordinate: i16) -> CellOccupancyState {
60 self.intervals
61 .iter()
62 .find(|interval| interval.range.start <= coordinate && coordinate < interval.range.end)
63 .map(|interval| interval.state)
64 .unwrap_or(CellOccupancyState::Unoccupied)
65 }
66
67 fn paint(&mut self, range: Range<i16>, state: CellOccupancyState) {
70 if range.start >= range.end {
71 return;
72 }
73
74 match self.intervals.last_mut() {
77 None => {
78 self.intervals.push(OccupiedInterval { range, state });
79 return;
80 }
81 Some(last) if last.range.end <= range.start => {
82 if last.state == state && last.range.end == range.start {
83 last.range.end = range.end;
84 } else {
85 self.intervals.push(OccupiedInterval { range, state });
86 }
87 return;
88 }
89 _ => {}
90 }
91
92 let mut result: SmallVec<[OccupiedInterval; 2]> = SmallVec::new();
93
94 for interval in &self.intervals {
96 if interval.range.end <= range.start {
97 result.push(interval.clone());
98 } else if interval.range.start < range.start {
99 result.push(OccupiedInterval { range: interval.range.start..range.start, state: interval.state });
100 }
101 }
102
103 match result.last_mut() {
105 Some(last) if last.state == state && last.range.end == range.start => last.range.end = range.end,
106 _ => result.push(OccupiedInterval { range: range.clone(), state }),
107 }
108
109 for interval in &self.intervals {
111 let trimmed = if interval.range.start >= range.end {
112 interval.clone()
113 } else if interval.range.end > range.end {
114 OccupiedInterval { range: range.end..interval.range.end, state: interval.state }
115 } else {
116 continue;
117 };
118 match result.last_mut() {
119 Some(last) if last.state == trimmed.state && last.range.end == trimmed.range.start => {
120 last.range.end = trimmed.range.end
121 }
122 _ => result.push(trimmed),
123 }
124 }
125
126 self.intervals = result;
127 }
128
129 fn collision_extent(&self, range: &Range<i16>) -> Option<i16> {
135 let interval = self
136 .intervals
137 .iter()
138 .rev()
139 .take_while(|interval| interval.range.end > range.start)
140 .find(|interval| interval.overlaps(range))?;
141 Some(interval.range.end - 1)
142 }
143}
144
145pub(crate) struct CellOccupancyMatrix {
151 columns: TrackCounts,
153 rows: TrackCounts,
155 row_intervals: Vec<TrackIntervals>,
157 column_intervals: Vec<TrackIntervals>,
159}
160
161impl Debug for CellOccupancyMatrix {
163 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164 writeln!(
165 f,
166 "Rows: neg_implicit={} explicit={} pos_implicit={}",
167 self.rows.negative_implicit, self.rows.explicit, self.rows.positive_implicit
168 )?;
169 writeln!(
170 f,
171 "Cols: neg_implicit={} explicit={} pos_implicit={}",
172 self.columns.negative_implicit, self.columns.explicit, self.columns.positive_implicit
173 )?;
174 if self.rows.len() > 100 || self.columns.len() > 100 {
175 writeln!(f, "State: (not printed: more than 100 tracks)")?;
176 return Ok(());
177 }
178 writeln!(f, "State:")?;
179
180 for row in &self.row_intervals {
181 for column_index in 0..self.columns.len() {
182 let coordinate = self.columns.track_to_prev_oz_line(column_index as u16);
183 let letter = match row.state_at(coordinate.0) {
184 CellOccupancyState::Unoccupied => '_',
185 CellOccupancyState::DefinitelyPlaced => 'D',
186 CellOccupancyState::AutoPlaced => 'A',
187 };
188 write!(f, "{letter}")?;
189 }
190 writeln!(f)?;
191 }
192
193 Ok(())
194 }
195}
196
197impl CellOccupancyMatrix {
198 pub fn with_track_counts(columns: TrackCounts, rows: TrackCounts) -> Self {
201 let mut row_intervals = new_vec_with_capacity(rows.len());
202 row_intervals.resize(rows.len(), TrackIntervals::default());
203 let mut column_intervals = new_vec_with_capacity(columns.len());
204 column_intervals.resize(columns.len(), TrackIntervals::default());
205 Self { rows, columns, row_intervals, column_intervals }
206 }
207
208 fn track_lists(&self, track_axis: AbsoluteAxis) -> &[TrackIntervals] {
211 match track_axis {
212 AbsoluteAxis::Horizontal => &self.column_intervals,
213 AbsoluteAxis::Vertical => &self.row_intervals,
214 }
215 }
216
217 fn expand_to_fit_range(&mut self, row_span: Line<OriginZeroLine>, col_span: Line<OriginZeroLine>) {
220 let req_negative_rows = max(-(self.rows.negative_implicit as i16) - row_span.start.0, 0);
222 let req_positive_rows = max(row_span.end.0 - self.rows.implicit_end_line().0, 0);
223 let req_negative_cols = max(-(self.columns.negative_implicit as i16) - col_span.start.0, 0);
224 let req_positive_cols = max(col_span.end.0 - self.columns.implicit_end_line().0, 0);
225
226 if req_negative_rows > 0 {
229 self.row_intervals
230 .splice(0..0, core::iter::repeat_with(TrackIntervals::default).take(req_negative_rows as usize));
231 }
232 if req_positive_rows > 0 {
233 let new_len = self.row_intervals.len() + req_positive_rows as usize;
234 self.row_intervals.resize(new_len, TrackIntervals::default());
235 }
236 if req_negative_cols > 0 {
237 self.column_intervals
238 .splice(0..0, core::iter::repeat_with(TrackIntervals::default).take(req_negative_cols as usize));
239 }
240 if req_positive_cols > 0 {
241 let new_len = self.column_intervals.len() + req_positive_cols as usize;
242 self.column_intervals.resize(new_len, TrackIntervals::default());
243 }
244
245 self.rows.negative_implicit += req_negative_rows as u16;
246 self.rows.positive_implicit += req_positive_rows as u16;
247 self.columns.negative_implicit += req_negative_cols as u16;
248 self.columns.positive_implicit += req_positive_cols as u16;
249 }
250
251 pub fn mark_area_as(
253 &mut self,
254 primary_axis: AbsoluteAxis,
255 primary_span: Line<OriginZeroLine>,
256 secondary_span: Line<OriginZeroLine>,
257 value: CellOccupancyState,
258 ) {
259 let (row_span, column_span) = match primary_axis {
260 AbsoluteAxis::Horizontal => (secondary_span, primary_span),
261 AbsoluteAxis::Vertical => (primary_span, secondary_span),
262 };
263
264 self.expand_to_fit_range(row_span, column_span);
265
266 if value == CellOccupancyState::AutoPlaced
267 && primary_span.start < primary_span.end
268 && secondary_span.start < secondary_span.end
269 {
270 self.advance_auto_placement_cursor(primary_axis, secondary_span.start, primary_span.end);
271 }
272
273 let row_range = self.rows.oz_line_range_to_track_range(row_span);
274 let col_range = self.columns.oz_line_range_to_track_range(column_span);
275 for row_index in row_range {
276 self.row_intervals[row_index as usize].paint(column_span.start.0..column_span.end.0, value);
277 }
278 for column_index in col_range {
279 self.column_intervals[column_index as usize].paint(row_span.start.0..row_span.end.0, value);
280 }
281 }
282
283 #[cfg(test)]
286 pub fn line_area_is_unoccupied(
287 &self,
288 primary_axis: AbsoluteAxis,
289 primary_span: Line<OriginZeroLine>,
290 secondary_span: Line<OriginZeroLine>,
291 ) -> bool {
292 self.line_area_collision_jump(primary_axis, primary_span, secondary_span).is_none()
293 }
294
295 pub fn line_area_collision_jump(
302 &self,
303 primary_axis: AbsoluteAxis,
304 primary_span: Line<OriginZeroLine>,
305 secondary_span: Line<OriginZeroLine>,
306 ) -> Option<OriginZeroLine> {
307 let track_lists = self.track_lists(primary_axis.other_axis());
308 let secondary_counts = self.track_counts(primary_axis.other_axis());
309 let secondary_range = secondary_counts.oz_line_range_to_track_range(secondary_span);
310
311 let secondary_start = max(secondary_range.start, 0);
314 let secondary_end = min(secondary_range.end, track_lists.len() as i16);
315
316 let primary_range = primary_span.start.0..primary_span.end.0;
317
318 let mut extent: Option<i16> = None;
319 for secondary_index in secondary_start..secondary_end {
320 let Some(cell) = track_lists[secondary_index as usize].collision_extent(&primary_range) else {
321 continue;
322 };
323 extent = Some(match extent {
324 None => cell,
325 Some(best) => max(best, cell),
326 });
327 }
328
329 extent.map(|cell| OriginZeroLine(cell + 1))
330 }
331
332 pub fn occupied_track_jump(&self, axis: AbsoluteAxis, span: Line<OriginZeroLine>) -> Option<OriginZeroLine> {
337 let counts = self.track_counts(axis);
338 let track_lists = self.track_lists(axis);
339 let range = counts.oz_line_range_to_track_range(span);
340 let start = max(range.start, 0);
341 let end = min(range.end, track_lists.len() as i16);
342 let found = (start..end).rev().find(|&index| !track_lists[index as usize].is_empty());
343 found.map(|track_index| {
344 let line = counts.track_to_prev_oz_line(track_index as u16);
345 line + 1
346 })
347 }
348
349 pub fn row_is_occupied(&self, row_index: usize) -> bool {
351 self.track_lists(AbsoluteAxis::Vertical).get(row_index).is_some_and(|track| !track.is_empty())
352 }
353
354 pub fn column_is_occupied(&self, column_index: usize) -> bool {
356 self.track_lists(AbsoluteAxis::Horizontal).get(column_index).is_some_and(|track| !track.is_empty())
357 }
358
359 pub fn track_counts(&self, track_type: AbsoluteAxis) -> &TrackCounts {
361 match track_type {
362 AbsoluteAxis::Horizontal => &self.columns,
363 AbsoluteAxis::Vertical => &self.rows,
364 }
365 }
366
367 pub fn auto_placement_cursor(&self, track_type: AbsoluteAxis, start_at: OriginZeroLine) -> Option<OriginZeroLine> {
369 let track_counts = self.track_counts(track_type.other_axis());
370 let track_computed_index = track_counts.oz_line_to_next_track(start_at);
371 let track_lists = self.track_lists(track_type.other_axis());
372 if track_computed_index < 0 || track_computed_index >= track_lists.len() as i16 {
373 return None;
375 }
376 track_lists[track_computed_index as usize].auto_placement_cursor
377 }
378
379 fn advance_auto_placement_cursor(
382 &mut self,
383 primary_axis: AbsoluteAxis,
384 secondary_start: OriginZeroLine,
385 primary_end: OriginZeroLine,
386 ) {
387 let track_index = self.track_counts(primary_axis.other_axis()).oz_line_to_next_track(secondary_start) as usize;
388 let tracks = match primary_axis {
389 AbsoluteAxis::Horizontal => &mut self.row_intervals,
390 AbsoluteAxis::Vertical => &mut self.column_intervals,
391 };
392 let cursor = &mut tracks[track_index].auto_placement_cursor;
393 *cursor = max(*cursor, Some(primary_end));
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn interval(range: Range<i16>, state: CellOccupancyState) -> OccupiedInterval {
402 OccupiedInterval { range, state }
403 }
404
405 mod track_intervals {
406 use super::*;
407 use CellOccupancyState::{AutoPlaced, DefinitelyPlaced};
408
409 #[test]
410 fn paint_merges_touching_same_state_intervals() {
411 let mut track = TrackIntervals::default();
412 track.paint(0..2, AutoPlaced);
413 track.paint(4..6, AutoPlaced);
414 track.paint(2..4, AutoPlaced);
415 assert_eq!(track.intervals.as_slice(), &[interval(0..6, AutoPlaced)]);
416 }
417
418 #[test]
419 fn paint_does_not_merge_different_states() {
420 let mut track = TrackIntervals::default();
421 track.paint(0..2, AutoPlaced);
422 track.paint(2..4, DefinitelyPlaced);
423 assert_eq!(track.intervals.as_slice(), &[interval(0..2, AutoPlaced), interval(2..4, DefinitelyPlaced)]);
424 }
425
426 #[test]
427 fn paint_overwrites_overlapped_cells() {
428 let mut track = TrackIntervals::default();
429 track.paint(0..6, AutoPlaced);
430 track.paint(2..4, DefinitelyPlaced);
431 assert_eq!(
432 track.intervals.as_slice(),
433 &[interval(0..2, AutoPlaced), interval(2..4, DefinitelyPlaced), interval(4..6, AutoPlaced)]
434 );
435
436 track.paint(-1..7, AutoPlaced);
438 assert_eq!(track.intervals.as_slice(), &[interval(-1..7, AutoPlaced)]);
439 }
440
441 #[test]
442 fn paint_overwrites_multiple_intervals() {
443 let mut track = TrackIntervals::default();
444 track.paint(0..2, AutoPlaced);
445 track.paint(3..5, DefinitelyPlaced);
446 track.paint(6..8, AutoPlaced);
447 track.paint(1..7, DefinitelyPlaced);
448 assert_eq!(
449 track.intervals.as_slice(),
450 &[interval(0..1, AutoPlaced), interval(1..7, DefinitelyPlaced), interval(7..8, AutoPlaced)]
451 );
452 }
453
454 #[test]
455 fn collision_extent_finds_extremal_occupied_cell() {
456 let mut track = TrackIntervals::default();
457 track.paint(2..4, AutoPlaced);
458 track.paint(6..8, DefinitelyPlaced);
459 assert_eq!(track.collision_extent(&(0..10)), Some(7));
461 assert_eq!(track.collision_extent(&(0..7)), Some(7));
462 assert_eq!(track.collision_extent(&(0..6)), Some(3));
463 assert_eq!(track.collision_extent(&(4..6)), None);
464 }
465
466 #[test]
467 fn state_at_distinguishes_occupied_intervals_and_gaps() {
468 let mut track = TrackIntervals::default();
469 track.paint(0..2, DefinitelyPlaced);
470 track.paint(2..4, AutoPlaced);
471 track.paint(6..8, AutoPlaced);
472 track.paint(8..9, DefinitelyPlaced);
473 assert_eq!(track.state_at(0), DefinitelyPlaced);
474 assert_eq!(track.state_at(3), AutoPlaced);
475 assert_eq!(track.state_at(5), CellOccupancyState::Unoccupied);
476 assert_eq!(track.state_at(7), AutoPlaced);
477 assert_eq!(track.state_at(8), DefinitelyPlaced);
478 }
479
480 #[test]
481 fn definitely_placed_overwrite_hides_auto_placed_cells() {
482 let mut track = TrackIntervals::default();
483 track.paint(0..4, CellOccupancyState::AutoPlaced);
484 track.paint(2..4, CellOccupancyState::DefinitelyPlaced);
485 assert_eq!(track.state_at(1), CellOccupancyState::AutoPlaced);
486 assert_eq!(track.state_at(2), CellOccupancyState::DefinitelyPlaced);
487 assert_eq!(track.state_at(3), CellOccupancyState::DefinitelyPlaced);
488 }
489 }
490
491 mod cell_occupancy_matrix {
492 use super::*;
493 use crate::geometry::AbsoluteAxis::{Horizontal, Vertical};
494 use CellOccupancyState::AutoPlaced;
495
496 fn line(start: i16, end: i16) -> Line<OriginZeroLine> {
497 Line { start: OriginZeroLine(start), end: OriginZeroLine(end) }
498 }
499
500 #[test]
501 fn negative_expansion_preserves_occupancy() {
502 let mut matrix =
503 CellOccupancyMatrix::with_track_counts(TrackCounts::from_raw(0, 2, 0), TrackCounts::from_raw(0, 2, 0));
504 matrix.mark_area_as(Horizontal, line(0, 1), line(0, 1), AutoPlaced);
505 matrix.mark_area_as(Horizontal, line(-2, -1), line(-1, 0), AutoPlaced);
507
508 assert_eq!(*matrix.track_counts(Horizontal), TrackCounts::from_raw(2, 2, 0));
509 assert_eq!(*matrix.track_counts(Vertical), TrackCounts::from_raw(1, 2, 0));
510
511 assert!(!matrix.line_area_is_unoccupied(Horizontal, line(0, 1), line(0, 1)));
513 assert!(!matrix.line_area_is_unoccupied(Horizontal, line(-2, -1), line(-1, 0)));
514 assert!(matrix.line_area_is_unoccupied(Horizontal, line(-1, 0), line(0, 1)));
515
516 assert!(matrix.column_is_occupied(0)); assert!(!matrix.column_is_occupied(1)); assert!(matrix.column_is_occupied(2)); assert!(matrix.row_is_occupied(0)); assert!(matrix.row_is_occupied(1)); assert!(!matrix.row_is_occupied(2)); }
524
525 #[test]
526 fn auto_placed_item_only_advances_cursor_in_first_secondary_track() {
527 for axis in [Horizontal, Vertical] {
528 let mut matrix = CellOccupancyMatrix::with_track_counts(
529 TrackCounts::from_raw(0, 4, 0),
530 TrackCounts::from_raw(0, 4, 0),
531 );
532 matrix.mark_area_as(axis, line(1, 3), line(1, 4), AutoPlaced);
533
534 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(1)), Some(OriginZeroLine(3)));
535 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(2)), None);
536 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(3)), None);
537
538 assert!(!matrix.line_area_is_unoccupied(axis, line(1, 3), line(2, 4)));
539 }
540 }
541
542 #[test]
543 fn cursors_survive_interval_merging_and_grid_expansion() {
544 for axis in [Horizontal, Vertical] {
545 let mut matrix = CellOccupancyMatrix::with_track_counts(
546 TrackCounts::from_raw(0, 4, 0),
547 TrackCounts::from_raw(0, 4, 0),
548 );
549 matrix.mark_area_as(axis, line(2, 4), line(1, 2), AutoPlaced);
550 matrix.mark_area_as(axis, line(4, 8), line(0, 2), AutoPlaced);
551 matrix.mark_area_as(axis, line(8, 10), line(1, 2), CellOccupancyState::DefinitelyPlaced);
552 matrix.mark_area_as(axis, line(-3, -1), line(-2, -1), AutoPlaced);
553 matrix.mark_area_as(axis, line(12, 14), line(7, 8), AutoPlaced);
554 matrix.mark_area_as(axis, line(0, 1), line(1, 2), AutoPlaced);
555
556 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(1)), Some(OriginZeroLine(4)));
557 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(0)), Some(OriginZeroLine(8)));
558 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(-2)), Some(OriginZeroLine(-1)));
559 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(7)), Some(OriginZeroLine(14)));
560 for track in [-3, -1, 2, 8] {
561 assert_eq!(matrix.auto_placement_cursor(axis, OriginZeroLine(track)), None);
562 }
563 }
564 }
565
566 #[test]
567 fn alternating_spans_merge_occupancy_and_skip_the_occupied_prefix() {
568 for axis in [Horizontal, Vertical] {
569 let mut matrix = CellOccupancyMatrix::with_track_counts(
570 TrackCounts::from_raw(0, 0, 0),
571 TrackCounts::from_raw(0, 0, 0),
572 );
573 for i in 0..128 {
574 matrix.mark_area_as(axis, line(i, i + 1), line(i % 2, 2), AutoPlaced);
575 }
576
577 let track = &matrix.track_lists(axis.other_axis())[1];
578 assert_eq!(track.intervals.as_slice(), &[interval(0..128, AutoPlaced)]);
579 assert!(!track.intervals.spilled());
580 assert_eq!(matrix.line_area_collision_jump(axis, line(0, 1), line(1, 2)), Some(OriginZeroLine(128)));
581 }
582 }
583
584 #[test]
585 fn collision_jump_returns_next_search_position() {
586 let mut matrix =
587 CellOccupancyMatrix::with_track_counts(TrackCounts::from_raw(0, 4, 0), TrackCounts::from_raw(0, 4, 0));
588 matrix.mark_area_as(Horizontal, line(1, 3), line(0, 1), AutoPlaced);
589
590 assert_eq!(matrix.line_area_collision_jump(Horizontal, line(0, 2), line(0, 1)), Some(OriginZeroLine(3)));
592 assert_eq!(matrix.line_area_collision_jump(Horizontal, line(0, 4), line(0, 1)), Some(OriginZeroLine(3)));
593 assert_eq!(matrix.line_area_collision_jump(Horizontal, line(0, 4), line(1, 2)), None);
595 }
596
597 #[test]
598 fn occupied_track_jump_skips_non_empty_tracks() {
599 let mut matrix =
600 CellOccupancyMatrix::with_track_counts(TrackCounts::from_raw(0, 4, 0), TrackCounts::from_raw(0, 4, 0));
601 matrix.mark_area_as(Horizontal, line(0, 1), line(1, 2), AutoPlaced);
602
603 assert_eq!(matrix.occupied_track_jump(Vertical, line(0, 4)), Some(OriginZeroLine(2)));
605 assert_eq!(matrix.occupied_track_jump(Vertical, line(2, 4)), None);
606 }
607 }
608}