Skip to main content

batuta/oracle/svg/
grid_protocol.rs

1//! SVG Grid Protocol Engine
2//!
3//! Cell-based 16x9 grid layout for 1080p video-optimized SVG generation.
4//! Provides provable non-overlap via occupied-set tracking and a manifest
5//! that documents all allocations as an XML comment.
6//!
7//! # Grid Geometry
8//!
9//! - 16 columns x 9 rows = 144 cells
10//! - Each cell is 120x120 pixels
11//! - 10px cell padding shrinks each allocation's render bounds
12//! - 20px internal padding further shrinks content zones
13//! - Canvas: 1920x1080 (16:9)
14
15use std::collections::HashSet;
16use std::fmt;
17
18// ── Constants ──────────────────────────────────────────────────────────────────
19
20/// Number of grid columns
21pub const GRID_COLS: u8 = 16;
22
23/// Number of grid rows
24pub const GRID_ROWS: u8 = 9;
25
26/// Size of each grid cell in pixels
27pub const CELL_SIZE: f32 = 120.0;
28
29/// Padding between cell edge and render bounds
30pub const CELL_PADDING: f32 = 10.0;
31
32/// Padding between render bounds and content zone
33pub const INTERNAL_PADDING: f32 = 20.0;
34
35/// Minimum gap between stroked/filtered boxes
36pub const MIN_BLOCK_GAP: f32 = 20.0;
37
38/// Total number of cells in the grid
39pub const TOTAL_CELLS: usize = (GRID_COLS as usize) * (GRID_ROWS as usize);
40
41/// Canvas width in pixels (16 * 120)
42pub const CANVAS_WIDTH: f32 = 1920.0;
43
44/// Canvas height in pixels (9 * 120)
45pub const CANVAS_HEIGHT: f32 = 1080.0;
46
47// ── PixelBounds ────────────────────────────────────────────────────────────────
48
49/// Axis-aligned rectangle in pixel coordinates.
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct PixelBounds {
52    pub x: f32,
53    pub y: f32,
54    pub w: f32,
55    pub h: f32,
56}
57
58impl PixelBounds {
59    /// Create new pixel bounds.
60    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
61        Self { x, y, w, h }
62    }
63
64    /// Right edge x coordinate.
65    pub fn right(&self) -> f32 {
66        self.x + self.w
67    }
68
69    /// Bottom edge y coordinate.
70    pub fn bottom(&self) -> f32 {
71        self.y + self.h
72    }
73
74    /// Center x coordinate.
75    pub fn cx(&self) -> f32 {
76        self.x + self.w / 2.0
77    }
78
79    /// Center y coordinate.
80    pub fn cy(&self) -> f32 {
81        self.y + self.h / 2.0
82    }
83}
84
85impl fmt::Display for PixelBounds {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "({}, {}, {}x{})", self.x, self.y, self.w, self.h)
88    }
89}
90
91// ── GridSpan ───────────────────────────────────────────────────────────────────
92
93/// A rectangular span of grid cells defined by top-left (c1, r1) and
94/// bottom-right (c2, r2) inclusive.
95///
96/// Coordinates are 0-based: columns 0..15, rows 0..8.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct GridSpan {
99    /// Left column (inclusive, 0-based)
100    pub c1: u8,
101    /// Top row (inclusive, 0-based)
102    pub r1: u8,
103    /// Right column (inclusive, 0-based)
104    pub c2: u8,
105    /// Bottom row (inclusive, 0-based)
106    pub r2: u8,
107}
108
109impl GridSpan {
110    /// Create a new grid span. `c2 >= c1` and `r2 >= r1` required.
111    pub fn new(c1: u8, r1: u8, c2: u8, r2: u8) -> Self {
112        Self { c1, r1, c2, r2 }
113    }
114
115    /// Raw pixel bounds: cell edges with no padding.
116    pub fn pixel_bounds(&self) -> PixelBounds {
117        let x = self.c1 as f32 * CELL_SIZE;
118        let y = self.r1 as f32 * CELL_SIZE;
119        let w = (self.c2 - self.c1 + 1) as f32 * CELL_SIZE;
120        let h = (self.r2 - self.r1 + 1) as f32 * CELL_SIZE;
121        PixelBounds::new(x, y, w, h)
122    }
123
124    /// Render bounds: raw bounds inset by CELL_PADDING (10px) on each side.
125    pub fn render_bounds(&self) -> PixelBounds {
126        let raw = self.pixel_bounds();
127        PixelBounds::new(
128            raw.x + CELL_PADDING,
129            raw.y + CELL_PADDING,
130            raw.w - 2.0 * CELL_PADDING,
131            raw.h - 2.0 * CELL_PADDING,
132        )
133    }
134
135    /// Content zone: render bounds inset by INTERNAL_PADDING (20px).
136    pub fn content_zone(&self) -> PixelBounds {
137        let rb = self.render_bounds();
138        PixelBounds::new(
139            rb.x + INTERNAL_PADDING,
140            rb.y + INTERNAL_PADDING,
141            rb.w - 2.0 * INTERNAL_PADDING,
142            rb.h - 2.0 * INTERNAL_PADDING,
143        )
144    }
145
146    /// Enumerate all (column, row) cells in this span.
147    pub fn cells(&self) -> Vec<(u8, u8)> {
148        let mut out = Vec::with_capacity(self.cell_count());
149        for r in self.r1..=self.r2 {
150            for c in self.c1..=self.c2 {
151                out.push((c, r));
152            }
153        }
154        out
155    }
156
157    /// Number of cells in this span.
158    pub fn cell_count(&self) -> usize {
159        (self.c2 - self.c1 + 1) as usize * (self.r2 - self.r1 + 1) as usize
160    }
161
162    /// Check if the span is within grid bounds.
163    fn is_in_bounds(&self) -> bool {
164        self.c1 <= self.c2 && self.r1 <= self.r2 && self.c2 < GRID_COLS && self.r2 < GRID_ROWS
165    }
166}
167
168impl fmt::Display for GridSpan {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        write!(f, "({},{})..({},{})", self.c1, self.r1, self.c2, self.r2)
171    }
172}
173
174// ── GridError ──────────────────────────────────────────────────────────────────
175
176/// Errors from grid allocation.
177#[derive(Debug, Clone)]
178pub enum GridError {
179    /// A cell in the requested span is already occupied.
180    CellOccupied { col: u8, row: u8, existing_name: String },
181    /// The span extends outside the 16x9 grid.
182    OutOfBounds { span: GridSpan },
183}
184
185impl fmt::Display for GridError {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        match self {
188            Self::CellOccupied { col, row, existing_name } => {
189                write!(f, "Cell ({}, {}) already occupied by '{}'", col, row, existing_name)
190            }
191            Self::OutOfBounds { span } => {
192                write!(f, "Span {} is outside the {}x{} grid", span, GRID_COLS, GRID_ROWS)
193            }
194        }
195    }
196}
197
198impl std::error::Error for GridError {}
199
200// ── Allocation (internal) ──────────────────────────────────────────────────────
201
202/// A recorded allocation in the grid.
203#[derive(Debug, Clone)]
204struct Allocation {
205    name: String,
206    span: GridSpan,
207    step: usize,
208}
209
210// ── GridProtocol ───────────────────────────────────────────────────────────────
211
212/// Occupied-set engine for provable non-overlap cell allocation.
213#[derive(Debug)]
214pub struct GridProtocol {
215    /// Set of occupied (col, row) cells.
216    occupied: HashSet<(u8, u8)>,
217    /// Name lookup for occupied cells (for error messages).
218    cell_owner: std::collections::HashMap<(u8, u8), String>,
219    /// Ordered allocation log.
220    allocations: Vec<Allocation>,
221}
222
223impl GridProtocol {
224    /// Create a new empty grid protocol.
225    pub fn new() -> Self {
226        Self {
227            occupied: HashSet::new(),
228            cell_owner: std::collections::HashMap::new(),
229            allocations: Vec::new(),
230        }
231    }
232
233    /// Allocate a named region. Returns the render bounds on success.
234    pub fn allocate(&mut self, name: &str, span: GridSpan) -> Result<PixelBounds, GridError> {
235        if !span.is_in_bounds() {
236            return Err(GridError::OutOfBounds { span });
237        }
238
239        // Check every cell for conflicts
240        for (c, r) in span.cells() {
241            if self.occupied.contains(&(c, r)) {
242                let existing = self.cell_owner.get(&(c, r)).cloned().unwrap_or_default();
243                return Err(GridError::CellOccupied { col: c, row: r, existing_name: existing });
244            }
245        }
246
247        // Mark cells occupied
248        let step = self.allocations.len();
249        for (c, r) in span.cells() {
250            self.occupied.insert((c, r));
251            self.cell_owner.insert((c, r), name.to_string());
252        }
253
254        self.allocations.push(Allocation { name: name.to_string(), span, step });
255
256        Ok(span.render_bounds())
257    }
258
259    /// Dry-run check: would this span succeed?
260    pub fn try_allocate(&self, span: &GridSpan) -> bool {
261        if !span.is_in_bounds() {
262            return false;
263        }
264        span.cells().iter().all(|cell| !self.occupied.contains(cell))
265    }
266
267    /// Number of occupied cells.
268    pub fn cells_used(&self) -> usize {
269        self.occupied.len()
270    }
271
272    /// Number of free cells.
273    pub fn cells_free(&self) -> usize {
274        TOTAL_CELLS - self.occupied.len()
275    }
276
277    /// Produce an XML comment manifest of all allocations.
278    pub fn manifest(&self) -> String {
279        let mut out = String::from("<!-- GRID PROTOCOL MANIFEST\n");
280        out.push_str(&format!(
281            "  Canvas: {}x{} | Grid: {}x{} | Cell: {}px\n",
282            CANVAS_WIDTH, CANVAS_HEIGHT, GRID_COLS, GRID_ROWS, CELL_SIZE
283        ));
284        out.push_str(&format!(
285            "  Cells used: {} / {} ({:.0}%)\n",
286            self.cells_used(),
287            TOTAL_CELLS,
288            self.cells_used() as f32 / TOTAL_CELLS as f32 * 100.0
289        ));
290        out.push_str("  Allocations:\n");
291        for alloc in &self.allocations {
292            let rb = alloc.span.render_bounds();
293            out.push_str(&format!(
294                "    [{}] \"{}\" span={} render=({},{},{}x{})\n",
295                alloc.step, alloc.name, alloc.span, rb.x, rb.y, rb.w, rb.h,
296            ));
297        }
298        out.push_str("-->");
299        out
300    }
301}
302
303impl Default for GridProtocol {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309// ── LayoutTemplate ─────────────────────────────────────────────────────────────
310
311/// Pre-built layout templates for common slide types (A-G).
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum LayoutTemplate {
314    /// A: Title slide — full-width title bar + centered subtitle
315    TitleSlide,
316    /// B: Two-column — left and right halves
317    TwoColumn,
318    /// C: Dashboard — header + 2x2 quadrants
319    Dashboard,
320    /// D: Code walkthrough — code left, notes right
321    CodeWalkthrough,
322    /// E: Diagram — header + full-width diagram area
323    Diagram,
324    /// F: Key concepts — header + 3-column cards
325    KeyConcepts,
326    /// G: Reflection & readings — header + two sections
327    ReflectionReadings,
328}
329
330impl LayoutTemplate {
331    /// Return the named allocations for this template.
332    pub fn allocations(&self) -> Vec<(&'static str, GridSpan)> {
333        match self {
334            Self::TitleSlide => vec![
335                ("title", GridSpan::new(1, 2, 14, 4)),
336                ("subtitle", GridSpan::new(2, 5, 13, 6)),
337            ],
338            Self::TwoColumn => vec![
339                ("header", GridSpan::new(0, 0, 15, 1)),
340                ("left", GridSpan::new(0, 2, 7, 8)),
341                ("right", GridSpan::new(8, 2, 15, 8)),
342            ],
343            Self::Dashboard => vec![
344                ("header", GridSpan::new(0, 0, 15, 1)),
345                ("top_left", GridSpan::new(0, 2, 7, 4)),
346                ("top_right", GridSpan::new(8, 2, 15, 4)),
347                ("bottom_left", GridSpan::new(0, 5, 7, 8)),
348                ("bottom_right", GridSpan::new(8, 5, 15, 8)),
349            ],
350            Self::CodeWalkthrough => vec![
351                ("header", GridSpan::new(0, 0, 15, 1)),
352                ("code", GridSpan::new(0, 2, 9, 8)),
353                ("notes", GridSpan::new(10, 2, 15, 8)),
354            ],
355            Self::Diagram => vec![
356                ("header", GridSpan::new(0, 0, 15, 1)),
357                ("diagram", GridSpan::new(0, 2, 15, 8)),
358            ],
359            Self::KeyConcepts => vec![
360                ("header", GridSpan::new(0, 0, 15, 1)),
361                ("card_left", GridSpan::new(0, 2, 4, 8)),
362                ("card_center", GridSpan::new(5, 2, 10, 8)),
363                ("card_right", GridSpan::new(11, 2, 15, 8)),
364            ],
365            Self::ReflectionReadings => vec![
366                ("header", GridSpan::new(0, 0, 15, 1)),
367                ("reflection", GridSpan::new(0, 2, 15, 5)),
368                ("readings", GridSpan::new(0, 6, 15, 8)),
369            ],
370        }
371    }
372
373    /// Allocate all regions for this template into the given protocol.
374    pub fn apply(
375        &self,
376        protocol: &mut GridProtocol,
377    ) -> Result<Vec<(&'static str, PixelBounds)>, GridError> {
378        let mut results = Vec::new();
379        for (name, span) in self.allocations() {
380            let bounds = protocol.allocate(name, span)?;
381            results.push((name, bounds));
382        }
383        Ok(results)
384    }
385}
386
387impl fmt::Display for LayoutTemplate {
388    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        match self {
390            Self::TitleSlide => write!(f, "A: Title Slide"),
391            Self::TwoColumn => write!(f, "B: Two Column"),
392            Self::Dashboard => write!(f, "C: Dashboard"),
393            Self::CodeWalkthrough => write!(f, "D: Code Walkthrough"),
394            Self::Diagram => write!(f, "E: Diagram"),
395            Self::KeyConcepts => write!(f, "F: Key Concepts"),
396            Self::ReflectionReadings => write!(f, "G: Reflection & Readings"),
397        }
398    }
399}
400
401// ── Tests ──────────────────────────────────────────────────────────────────────
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    #[test]
408    fn test_constants() {
409        assert_eq!(GRID_COLS, 16);
410        assert_eq!(GRID_ROWS, 9);
411        assert_eq!(CELL_SIZE, 120.0);
412        assert_eq!(TOTAL_CELLS, 144);
413        assert_eq!(CANVAS_WIDTH, 1920.0);
414        assert_eq!(CANVAS_HEIGHT, 1080.0);
415    }
416
417    #[test]
418    fn test_pixel_bounds() {
419        let pb = PixelBounds::new(10.0, 20.0, 100.0, 50.0);
420        assert_eq!(pb.right(), 110.0);
421        assert_eq!(pb.bottom(), 70.0);
422        assert_eq!(pb.cx(), 60.0);
423        assert_eq!(pb.cy(), 45.0);
424    }
425
426    #[test]
427    fn test_pixel_bounds_display() {
428        let pb = PixelBounds::new(10.0, 20.0, 100.0, 50.0);
429        assert_eq!(format!("{}", pb), "(10, 20, 100x50)");
430    }
431
432    #[test]
433    fn test_grid_span_pixel_bounds() {
434        // Single cell at (0, 0)
435        let span = GridSpan::new(0, 0, 0, 0);
436        let pb = span.pixel_bounds();
437        assert_eq!(pb.x, 0.0);
438        assert_eq!(pb.y, 0.0);
439        assert_eq!(pb.w, 120.0);
440        assert_eq!(pb.h, 120.0);
441
442        // 2x2 block at (1, 1)
443        let span = GridSpan::new(1, 1, 2, 2);
444        let pb = span.pixel_bounds();
445        assert_eq!(pb.x, 120.0);
446        assert_eq!(pb.y, 120.0);
447        assert_eq!(pb.w, 240.0);
448        assert_eq!(pb.h, 240.0);
449    }
450
451    #[test]
452    fn test_grid_span_render_bounds() {
453        let span = GridSpan::new(0, 0, 0, 0);
454        let rb = span.render_bounds();
455        assert_eq!(rb.x, 10.0);
456        assert_eq!(rb.y, 10.0);
457        assert_eq!(rb.w, 100.0);
458        assert_eq!(rb.h, 100.0);
459    }
460
461    #[test]
462    fn test_grid_span_content_zone() {
463        let span = GridSpan::new(0, 0, 0, 0);
464        let cz = span.content_zone();
465        assert_eq!(cz.x, 30.0);
466        assert_eq!(cz.y, 30.0);
467        assert_eq!(cz.w, 60.0);
468        assert_eq!(cz.h, 60.0);
469    }
470
471    #[test]
472    fn test_grid_span_cells() {
473        let span = GridSpan::new(1, 2, 2, 3);
474        let cells = span.cells();
475        assert_eq!(cells.len(), 4);
476        assert!(cells.contains(&(1, 2)));
477        assert!(cells.contains(&(2, 2)));
478        assert!(cells.contains(&(1, 3)));
479        assert!(cells.contains(&(2, 3)));
480    }
481
482    #[test]
483    fn test_grid_span_cell_count() {
484        assert_eq!(GridSpan::new(0, 0, 0, 0).cell_count(), 1);
485        assert_eq!(GridSpan::new(0, 0, 1, 1).cell_count(), 4);
486        assert_eq!(GridSpan::new(0, 0, 15, 8).cell_count(), 144);
487    }
488
489    #[test]
490    fn test_grid_span_display() {
491        let span = GridSpan::new(1, 2, 3, 4);
492        assert_eq!(format!("{}", span), "(1,2)..(3,4)");
493    }
494
495    #[test]
496    fn test_grid_protocol_allocate() {
497        let mut gp = GridProtocol::new();
498        let result = gp.allocate("header", GridSpan::new(0, 0, 15, 1));
499        assert!(result.is_ok());
500        assert_eq!(gp.cells_used(), 32); // 16 * 2
501        assert_eq!(gp.cells_free(), 144 - 32);
502    }
503
504    #[test]
505    fn test_grid_protocol_overlap_rejected() {
506        let mut gp = GridProtocol::new();
507        gp.allocate("header", GridSpan::new(0, 0, 15, 1)).expect("unexpected failure");
508
509        let result = gp.allocate("overlap", GridSpan::new(5, 0, 10, 2));
510        assert!(result.is_err());
511        match result.unwrap_err() {
512            GridError::CellOccupied { col, row, existing_name } => {
513                assert!((5..=10).contains(&col));
514                assert_eq!(row, 0);
515                assert_eq!(existing_name, "header");
516            }
517            other => panic!("Expected CellOccupied, got: {}", other),
518        }
519    }
520
521    #[test]
522    fn test_grid_protocol_out_of_bounds() {
523        let mut gp = GridProtocol::new();
524        let result = gp.allocate("oob", GridSpan::new(0, 0, 16, 0));
525        assert!(result.is_err());
526        assert!(matches!(result.unwrap_err(), GridError::OutOfBounds { .. }));
527    }
528
529    #[test]
530    fn test_grid_protocol_try_allocate() {
531        let mut gp = GridProtocol::new();
532        let span = GridSpan::new(0, 0, 3, 3);
533        assert!(gp.try_allocate(&span));
534
535        gp.allocate("block", span).expect("unexpected failure");
536        assert!(!gp.try_allocate(&span));
537
538        // Adjacent span should be free
539        assert!(gp.try_allocate(&GridSpan::new(4, 0, 7, 3)));
540
541        // Out of bounds
542        assert!(!gp.try_allocate(&GridSpan::new(0, 0, 16, 0)));
543    }
544
545    #[test]
546    fn test_grid_protocol_manifest() {
547        let mut gp = GridProtocol::new();
548        gp.allocate("header", GridSpan::new(0, 0, 15, 1)).expect("unexpected failure");
549        gp.allocate("body", GridSpan::new(0, 2, 15, 8)).expect("unexpected failure");
550
551        let manifest = gp.manifest();
552        assert!(manifest.contains("GRID PROTOCOL MANIFEST"));
553        assert!(manifest.contains("\"header\""));
554        assert!(manifest.contains("\"body\""));
555        assert!(manifest.contains("Cells used: 144"));
556    }
557
558    #[test]
559    fn test_grid_protocol_default() {
560        let gp = GridProtocol::default();
561        assert_eq!(gp.cells_used(), 0);
562        assert_eq!(gp.cells_free(), 144);
563    }
564
565    #[test]
566    fn test_grid_error_display() {
567        let err = GridError::CellOccupied { col: 5, row: 3, existing_name: "header".to_string() };
568        let msg = format!("{}", err);
569        assert!(msg.contains("(5, 3)"));
570        assert!(msg.contains("header"));
571
572        let err = GridError::OutOfBounds { span: GridSpan::new(0, 0, 16, 0) };
573        let msg = format!("{}", err);
574        assert!(msg.contains("outside"));
575    }
576
577    #[test]
578    fn test_no_cell_in_two_allocations() {
579        let mut gp = GridProtocol::new();
580        gp.allocate("a", GridSpan::new(0, 0, 7, 4)).expect("unexpected failure");
581        gp.allocate("b", GridSpan::new(8, 0, 15, 4)).expect("unexpected failure");
582        gp.allocate("c", GridSpan::new(0, 5, 15, 8)).expect("unexpected failure");
583
584        // Full grid is occupied
585        assert_eq!(gp.cells_used(), 144);
586        assert_eq!(gp.cells_free(), 0);
587    }
588
589    // ── Layout Template tests ──────────────────────────────────────────────
590
591    #[test]
592    fn test_layout_template_title_slide() {
593        let mut gp = GridProtocol::new();
594        let result = LayoutTemplate::TitleSlide.apply(&mut gp);
595        assert!(result.is_ok());
596        let allocs = result.expect("operation failed");
597        assert_eq!(allocs.len(), 2);
598        assert_eq!(allocs[0].0, "title");
599        assert_eq!(allocs[1].0, "subtitle");
600    }
601
602    #[test]
603    fn test_layout_template_two_column() {
604        let mut gp = GridProtocol::new();
605        let result = LayoutTemplate::TwoColumn.apply(&mut gp);
606        assert!(result.is_ok());
607        let allocs = result.expect("operation failed");
608        assert_eq!(allocs.len(), 3);
609    }
610
611    #[test]
612    fn test_layout_template_dashboard() {
613        let mut gp = GridProtocol::new();
614        let result = LayoutTemplate::Dashboard.apply(&mut gp);
615        assert!(result.is_ok());
616        let allocs = result.expect("operation failed");
617        assert_eq!(allocs.len(), 5);
618    }
619
620    #[test]
621    fn test_layout_template_code_walkthrough() {
622        let mut gp = GridProtocol::new();
623        let result = LayoutTemplate::CodeWalkthrough.apply(&mut gp);
624        assert!(result.is_ok());
625        let allocs = result.expect("operation failed");
626        assert_eq!(allocs.len(), 3);
627        assert_eq!(allocs[1].0, "code");
628        assert_eq!(allocs[2].0, "notes");
629    }
630
631    #[test]
632    fn test_layout_template_diagram() {
633        let mut gp = GridProtocol::new();
634        let result = LayoutTemplate::Diagram.apply(&mut gp);
635        assert!(result.is_ok());
636        let allocs = result.expect("operation failed");
637        assert_eq!(allocs.len(), 2);
638        assert_eq!(allocs[0].0, "header");
639        assert_eq!(allocs[1].0, "diagram");
640    }
641
642    #[test]
643    fn test_layout_template_key_concepts() {
644        let mut gp = GridProtocol::new();
645        let result = LayoutTemplate::KeyConcepts.apply(&mut gp);
646        assert!(result.is_ok());
647        assert_eq!(result.expect("operation failed").len(), 4);
648    }
649
650    #[test]
651    fn test_layout_template_reflection_readings() {
652        let mut gp = GridProtocol::new();
653        let result = LayoutTemplate::ReflectionReadings.apply(&mut gp);
654        assert!(result.is_ok());
655        assert_eq!(result.expect("operation failed").len(), 3);
656    }
657
658    #[test]
659    fn test_layout_template_no_overlaps() {
660        // Every template must produce non-overlapping allocations
661        let templates = [
662            LayoutTemplate::TitleSlide,
663            LayoutTemplate::TwoColumn,
664            LayoutTemplate::Dashboard,
665            LayoutTemplate::CodeWalkthrough,
666            LayoutTemplate::Diagram,
667            LayoutTemplate::KeyConcepts,
668            LayoutTemplate::ReflectionReadings,
669        ];
670
671        for template in &templates {
672            let mut gp = GridProtocol::new();
673            let result = template.apply(&mut gp);
674            assert!(
675                result.is_ok(),
676                "Template {} has overlapping allocations: {:?}",
677                template,
678                result.unwrap_err()
679            );
680        }
681    }
682
683    #[test]
684    fn test_layout_template_display() {
685        assert_eq!(format!("{}", LayoutTemplate::TitleSlide), "A: Title Slide");
686        assert_eq!(format!("{}", LayoutTemplate::TwoColumn), "B: Two Column");
687        assert_eq!(format!("{}", LayoutTemplate::Dashboard), "C: Dashboard");
688        assert_eq!(format!("{}", LayoutTemplate::CodeWalkthrough), "D: Code Walkthrough");
689        assert_eq!(format!("{}", LayoutTemplate::Diagram), "E: Diagram");
690        assert_eq!(format!("{}", LayoutTemplate::KeyConcepts), "F: Key Concepts");
691        assert_eq!(format!("{}", LayoutTemplate::ReflectionReadings), "G: Reflection & Readings");
692    }
693
694    #[test]
695    fn test_canvas_dimensions_match_grid() {
696        assert_eq!(CANVAS_WIDTH, GRID_COLS as f32 * CELL_SIZE);
697        assert_eq!(CANVAS_HEIGHT, GRID_ROWS as f32 * CELL_SIZE);
698    }
699
700    #[test]
701    fn test_full_grid_span() {
702        let full = GridSpan::new(0, 0, 15, 8);
703        assert_eq!(full.cell_count(), 144);
704        let pb = full.pixel_bounds();
705        assert_eq!(pb.w, CANVAS_WIDTH);
706        assert_eq!(pb.h, CANVAS_HEIGHT);
707    }
708
709    #[test]
710    fn test_render_bounds_shrink() {
711        let span = GridSpan::new(0, 0, 1, 1);
712        let raw = span.pixel_bounds();
713        let render = span.render_bounds();
714        assert_eq!(render.x, raw.x + CELL_PADDING);
715        assert_eq!(render.y, raw.y + CELL_PADDING);
716        assert_eq!(render.w, raw.w - 2.0 * CELL_PADDING);
717        assert_eq!(render.h, raw.h - 2.0 * CELL_PADDING);
718    }
719
720    #[test]
721    fn test_content_zone_shrink() {
722        let span = GridSpan::new(0, 0, 3, 3);
723        let render = span.render_bounds();
724        let content = span.content_zone();
725        assert_eq!(content.x, render.x + INTERNAL_PADDING);
726        assert_eq!(content.y, render.y + INTERNAL_PADDING);
727        assert_eq!(content.w, render.w - 2.0 * INTERNAL_PADDING);
728        assert_eq!(content.h, render.h - 2.0 * INTERNAL_PADDING);
729    }
730
731    #[test]
732    fn test_grid_protocol_sequential_allocations() {
733        let mut gp = GridProtocol::new();
734
735        // Tile the grid with 4x3 blocks (4 across, 3 down)
736        for row_block in 0..3u8 {
737            for col_block in 0..4u8 {
738                let name = format!("block_{}_{}", col_block, row_block);
739                let c1 = col_block * 4;
740                let r1 = row_block * 3;
741                let result = gp.allocate(&name, GridSpan::new(c1, r1, c1 + 3, r1 + 2));
742                assert!(result.is_ok(), "Failed to allocate {}: {:?}", name, result);
743            }
744        }
745        assert_eq!(gp.cells_used(), 144);
746    }
747}