Skip to main content

hojicha_rendering/
algorithms.rs

1//! Core algorithms for differential rendering.
2//!
3//! This module contains the fundamental algorithms used in the differential
4//! rendering system, with detailed documentation, complexity analysis, and
5//! references to academic literature.
6
7use ratatui::layout::Rect;
8use std::collections::HashSet;
9
10/// Rectangle intersection algorithm using the Separating Axis Theorem.
11///
12/// # Theory
13///
14/// The Separating Axis Theorem states that two convex polygons intersect
15/// if and only if there is no separating axis between them. For axis-aligned
16/// rectangles, we only need to check the X and Y axes.
17///
18/// # Algorithm
19///
20/// Two rectangles A and B intersect if and only if:
21/// - They overlap on the X-axis AND
22/// - They overlap on the Y-axis
23///
24/// They DON'T overlap on an axis if:
25/// - A is completely to one side of B on that axis
26///
27/// # Complexity
28///
29/// - Time: O(1) - exactly 4 comparisons
30/// - Space: O(1) - no additional memory
31///
32/// # References
33///
34/// - "Real-Time Collision Detection" by Christer Ericson, Chapter 4
35/// - "Computational Geometry: Algorithms and Applications" by de Berg et al.
36///
37/// # Examples
38///
39/// ```rust
40/// use hojicha_rendering::algorithms::rectangles_intersect;
41/// use ratatui::layout::Rect;
42///
43/// let a = Rect::new(0, 0, 10, 10);
44/// let b = Rect::new(5, 5, 10, 10);  // overlapping
45/// let c = Rect::new(20, 20, 10, 10); // separate
46///
47/// assert!(rectangles_intersect(&a, &b));
48/// assert!(!rectangles_intersect(&a, &c));
49/// ```
50pub fn rectangles_intersect(a: &Rect, b: &Rect) -> bool {
51    debug_assert!(
52        a.width > 0 && a.height > 0,
53        "Rectangle A must have positive area"
54    );
55    debug_assert!(
56        b.width > 0 && b.height > 0,
57        "Rectangle B must have positive area"
58    );
59
60    // Check if rectangles are separated on X-axis
61    let x_separated = a.x >= b.x.saturating_add(b.width) || b.x >= a.x.saturating_add(a.width);
62
63    // Check if rectangles are separated on Y-axis
64    let y_separated = a.y >= b.y.saturating_add(b.height) || b.y >= a.y.saturating_add(a.height);
65
66    // Intersect if NOT separated on either axis
67    !x_separated && !y_separated
68}
69
70/// Rectangle union (bounding box) calculation.
71///
72/// Computes the smallest rectangle that contains both input rectangles.
73/// This is used in region merging to create the combined dirty area.
74///
75/// # Algorithm
76///
77/// 1. Find minimum X and Y coordinates (top-left corner)
78/// 2. Find maximum X+width and Y+height coordinates (bottom-right corner)
79/// 3. Compute width and height from corners
80///
81/// # Complexity
82///
83/// - Time: O(1) - arithmetic operations only
84/// - Space: O(1) - no additional memory allocation
85///
86/// # Overflow Handling
87///
88/// Uses saturating arithmetic to handle coordinate overflow gracefully.
89/// Large coordinates are clamped to u16::MAX rather than wrapping.
90///
91/// # Examples
92///
93/// ```rust
94/// use hojicha_rendering::algorithms::rectangle_union;
95/// use ratatui::layout::Rect;
96///
97/// let a = Rect::new(0, 0, 10, 10);
98/// let b = Rect::new(5, 5, 10, 10);
99/// let union = rectangle_union(&a, &b);
100///
101/// assert_eq!(union, Rect::new(0, 0, 15, 15));
102/// ```
103pub fn rectangle_union(a: &Rect, b: &Rect) -> Rect {
104    debug_assert!(
105        a.width > 0 && a.height > 0,
106        "Rectangle A must have positive area"
107    );
108    debug_assert!(
109        b.width > 0 && b.height > 0,
110        "Rectangle B must have positive area"
111    );
112
113    let min_x = a.x.min(b.x);
114    let min_y = a.y.min(b.y);
115
116    let max_x = a.x.saturating_add(a.width).max(b.x.saturating_add(b.width));
117    let max_y =
118        a.y.saturating_add(a.height)
119            .max(b.y.saturating_add(b.height));
120
121    let width = max_x.saturating_sub(min_x);
122    let height = max_y.saturating_sub(min_y);
123
124    debug_assert!(width > 0 && height > 0, "Union must have positive area");
125
126    Rect {
127        x: min_x,
128        y: min_y,
129        width,
130        height,
131    }
132}
133
134/// Check if two rectangles are adjacent (touching but not overlapping).
135///
136/// Two rectangles are adjacent if they share a border but have no
137/// overlapping area. This is used to determine if regions can be
138/// merged even when they don't overlap.
139///
140/// # Algorithm
141///
142/// Checks for adjacency in both directions:
143/// 1. **Horizontal adjacency**: Right edge of A touches left edge of B (or vice versa)
144///    AND they overlap on the Y-axis
145/// 2. **Vertical adjacency**: Bottom edge of A touches top edge of B (or vice versa)
146///    AND they overlap on the X-axis
147///
148/// # Complexity
149///
150/// - Time: O(1) - up to 8 comparisons
151/// - Space: O(1) - no additional memory
152///
153/// # Edge Cases
154///
155/// - Handles zero-width or zero-height rectangles gracefully
156/// - Uses saturating arithmetic for overflow protection
157/// - Returns false for overlapping rectangles (use intersection test instead)
158///
159/// # Examples
160///
161/// ```rust
162/// use hojicha_rendering::algorithms::rectangles_adjacent;
163/// use ratatui::layout::Rect;
164///
165/// // Horizontally adjacent
166/// let a = Rect::new(0, 0, 10, 10);
167/// let b = Rect::new(10, 0, 10, 10);
168/// assert!(rectangles_adjacent(&a, &b));
169///
170/// // Vertically adjacent  
171/// let c = Rect::new(0, 10, 10, 10);
172/// assert!(rectangles_adjacent(&a, &c));
173///
174/// // Not adjacent (gap between)
175/// let d = Rect::new(12, 0, 10, 10);
176/// assert!(!rectangles_adjacent(&a, &d));
177/// ```
178pub fn rectangles_adjacent(a: &Rect, b: &Rect) -> bool {
179    debug_assert!(
180        a.width > 0 && a.height > 0,
181        "Rectangle A must have positive area"
182    );
183    debug_assert!(
184        b.width > 0 && b.height > 0,
185        "Rectangle B must have positive area"
186    );
187
188    // Check horizontal adjacency (touching left/right edges)
189    let h_adjacent = (a.x.saturating_add(a.width) == b.x ||
190                     b.x.saturating_add(b.width) == a.x) &&
191                     // Must overlap on Y-axis
192                     !(a.y >= b.y.saturating_add(b.height) ||
193                       a.y.saturating_add(a.height) <= b.y);
194
195    // Check vertical adjacency (touching top/bottom edges)
196    let v_adjacent = (a.y.saturating_add(a.height) == b.y ||
197                     b.y.saturating_add(b.height) == a.y) &&
198                     // Must overlap on X-axis
199                     !(a.x >= b.x.saturating_add(b.width) ||
200                       a.x.saturating_add(a.width) <= b.x);
201
202    h_adjacent || v_adjacent
203}
204
205/// Greedy region coalescing algorithm.
206///
207/// Merges a collection of rectangles to reduce the total number while
208/// maintaining coverage. This reduces the number of render operations
209/// at the cost of potentially drawing slightly larger areas.
210///
211/// # Algorithm
212///
213/// Uses a greedy approach:
214/// 1. For each rectangle, try to merge with all others
215/// 2. If merge is beneficial (reduces total count), perform it
216/// 3. Continue until no more beneficial merges are found
217///
218/// # Complexity
219///
220/// - Time: O(n²) in worst case, O(n) in best case
221/// - Space: O(1) additional (modifies input in-place)
222/// - n = number of input rectangles
223///
224/// # Merge Criteria
225///
226/// Two rectangles are merged if:
227/// - They intersect OR are adjacent
228/// - The resulting area is not significantly larger than the sum
229///   (to avoid merging distant small regions)
230///
231/// # Trade-offs
232///
233/// - **Pros**: Reduces number of render calls, improves cache locality
234/// - **Cons**: May render slightly more pixels than strictly necessary
235/// - **Balance**: Configurable area growth threshold
236///
237/// # Examples
238///
239/// ```rust
240/// use hojicha_rendering::algorithms::coalesce_regions;
241/// use ratatui::layout::Rect;
242///
243/// let mut regions = vec![
244///     Rect::new(0, 0, 10, 10),
245///     Rect::new(10, 0, 10, 10),  // Adjacent to first
246///     Rect::new(5, 5, 5, 5),     // Overlaps with first
247/// ];
248///
249/// let original_count = regions.len();
250/// coalesce_regions(&mut regions, 1.5); // Allow 50% area growth
251///
252/// assert!(regions.len() < original_count); // Should have merged some
253/// ```
254pub fn coalesce_regions(regions: &mut Vec<Rect>, max_area_growth: f32) -> usize {
255    debug_assert!(max_area_growth >= 1.0, "Area growth factor must be >= 1.0");
256
257    let mut merged_count = 0;
258    let mut changed = true;
259
260    // Continue until no more merges are possible
261    while changed && regions.len() > 1 {
262        changed = false;
263
264        // Try to merge each pair of rectangles
265        for i in 0..regions.len() {
266            for j in (i + 1)..regions.len() {
267                let a = regions[i];
268                let b = regions[j];
269
270                // Check if rectangles can be merged
271                if rectangles_intersect(&a, &b) || rectangles_adjacent(&a, &b) {
272                    let union = rectangle_union(&a, &b);
273
274                    // Calculate area growth factor
275                    let original_area =
276                        (a.width as u32 * a.height as u32) + (b.width as u32 * b.height as u32);
277                    let union_area = union.width as u32 * union.height as u32;
278                    let growth_factor = union_area as f32 / original_area as f32;
279
280                    // Merge if growth is acceptable
281                    if growth_factor <= max_area_growth {
282                        regions[i] = union;
283                        regions.remove(j);
284                        merged_count += 1;
285                        changed = true;
286                        break;
287                    }
288                }
289            }
290            if changed {
291                break; // Restart from beginning after a merge
292            }
293        }
294    }
295
296    merged_count
297}
298
299/// Topological sort for component dependency resolution.
300///
301/// Sorts components based on their dependencies to ensure that dependent
302/// components are updated after their dependencies. This prevents stale
303/// data from being used in rendering.
304///
305/// # Algorithm
306///
307/// Uses Kahn's algorithm for topological sorting:
308/// 1. Find all nodes with no incoming edges (no dependencies)
309/// 2. Remove a node and all its outgoing edges
310/// 3. If this creates new nodes with no incoming edges, add them to queue
311/// 4. Repeat until all nodes are processed or a cycle is detected
312///
313/// # Complexity
314///
315/// - Time: O(V + E) where V is vertices (components) and E is edges (dependencies)
316/// - Space: O(V + E) for auxiliary data structures
317///
318/// # Cycle Detection
319///
320/// Returns `None` if a cycle is detected in the dependency graph.
321/// This indicates a configuration error where components have circular dependencies.
322///
323/// # Parameters
324///
325/// - `dependencies`: Map from component ID to set of dependency IDs
326///
327/// # Returns
328///
329/// - `Some(Vec<String>)`: Topologically sorted component IDs
330/// - `None`: Cycle detected in dependency graph
331///
332/// # Examples
333///
334/// ```rust
335/// use hojicha_rendering::algorithms::topological_sort;
336/// use std::collections::{HashMap, HashSet};
337///
338/// let mut deps = HashMap::new();
339/// deps.insert("C".to_string(), ["A", "B"].iter().map(|s| s.to_string()).collect());
340/// deps.insert("B".to_string(), ["A"].iter().map(|s| s.to_string()).collect());
341/// deps.insert("A".to_string(), HashSet::new());
342///
343/// let sorted = topological_sort(&deps).unwrap();
344/// // A comes before B, B comes before C
345/// assert_eq!(sorted, vec!["A", "B", "C"]);
346/// ```
347pub fn topological_sort(
348    dependencies: &std::collections::HashMap<String, HashSet<String>>,
349) -> Option<Vec<String>> {
350    use std::collections::{HashMap, VecDeque};
351
352    // Build reverse dependency graph (who depends on whom)
353    let mut in_degree: HashMap<String, usize> = HashMap::new();
354    let mut out_edges: HashMap<String, Vec<String>> = HashMap::new();
355
356    // Initialize all components
357    for component in dependencies.keys() {
358        in_degree.entry(component.clone()).or_insert(0);
359        out_edges.entry(component.clone()).or_default();
360    }
361
362    // Build the graph
363    for (component, deps) in dependencies {
364        for dep in deps {
365            // Ensure dependency exists in our graph
366            in_degree.entry(dep.clone()).or_insert(0);
367            out_edges.entry(dep.clone()).or_default();
368
369            // Add edge from dependency to component
370            out_edges.get_mut(dep).unwrap().push(component.clone());
371            *in_degree.get_mut(component).unwrap() += 1;
372        }
373    }
374
375    // Find all components with no dependencies
376    let mut queue: VecDeque<String> = in_degree
377        .iter()
378        .filter(|(_, &degree)| degree == 0)
379        .map(|(component, _)| component.clone())
380        .collect();
381
382    let mut result = Vec::new();
383
384    // Process components in dependency order
385    while let Some(component) = queue.pop_front() {
386        result.push(component.clone());
387
388        // Remove this component and update dependents
389        if let Some(dependents) = out_edges.get(&component) {
390            for dependent in dependents {
391                if let Some(degree) = in_degree.get_mut(dependent) {
392                    *degree -= 1;
393                    if *degree == 0 {
394                        queue.push_back(dependent.clone());
395                    }
396                }
397            }
398        }
399    }
400
401    // Check if all components were processed (no cycles)
402    if result.len() == dependencies.len() {
403        Some(result)
404    } else {
405        None // Cycle detected
406    }
407}
408
409/// FNV-1a hash function for fast checksumming.
410///
411/// Implements the FNV-1a (Fowler–Noll–Vo) hash algorithm, which provides
412/// good distribution and performance for small to medium-sized inputs
413/// like UI content.
414///
415/// # Algorithm
416///
417/// ```text
418/// hash = FNV_offset_basis
419/// for each byte in input:
420///     hash = hash XOR byte
421///     hash = hash * FNV_prime
422/// ```
423///
424/// # Properties
425///
426/// - **Fast**: Single pass, simple operations
427/// - **Good distribution**: Low collision rate for typical inputs
428/// - **Deterministic**: Same input always produces same hash
429/// - **Not cryptographic**: Don't use for security purposes
430///
431/// # Complexity
432///
433/// - Time: O(n) where n is input length
434/// - Space: O(1) - constant memory usage
435///
436/// # Parameters
437///
438/// - `data`: Byte slice to hash
439///
440/// # Returns
441///
442/// 64-bit hash value
443///
444/// # References
445///
446/// - http://www.isthe.com/chongo/tech/comp/fnv/
447/// - RFC: https://tools.ietf.org/id/draft-eastlake-fnv-16.html
448///
449/// # Examples
450///
451/// ```rust
452/// use hojicha_rendering::algorithms::fnv1a_hash;
453///
454/// let hash1 = fnv1a_hash(b"hello");
455/// let hash2 = fnv1a_hash(b"hello");
456/// let hash3 = fnv1a_hash(b"world");
457///
458/// assert_eq!(hash1, hash2); // Same input = same hash
459/// assert_ne!(hash1, hash3); // Different input = different hash (usually)
460/// ```
461pub fn fnv1a_hash(data: &[u8]) -> u64 {
462    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
463    const FNV_PRIME: u64 = 0x100000001b3;
464
465    let mut hash = FNV_OFFSET_BASIS;
466
467    for &byte in data {
468        hash ^= byte as u64;
469        hash = hash.wrapping_mul(FNV_PRIME);
470    }
471
472    hash
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use std::collections::{HashMap, HashSet};
479
480    #[test]
481    fn test_rectangles_intersect() {
482        let a = Rect::new(0, 0, 10, 10);
483
484        // Overlapping
485        assert!(rectangles_intersect(&a, &Rect::new(5, 5, 10, 10)));
486
487        // Touching edges (not overlapping)
488        assert!(!rectangles_intersect(&a, &Rect::new(10, 0, 10, 10)));
489
490        // Completely separate
491        assert!(!rectangles_intersect(&a, &Rect::new(20, 20, 10, 10)));
492
493        // Contained
494        assert!(rectangles_intersect(&a, &Rect::new(2, 2, 5, 5)));
495    }
496
497    #[test]
498    fn test_rectangle_union() {
499        let a = Rect::new(0, 0, 10, 10);
500        let b = Rect::new(5, 5, 10, 10);
501
502        let union = rectangle_union(&a, &b);
503        assert_eq!(union, Rect::new(0, 0, 15, 15));
504
505        // Union with non-overlapping
506        let c = Rect::new(20, 20, 10, 10);
507        let union2 = rectangle_union(&a, &c);
508        assert_eq!(union2, Rect::new(0, 0, 30, 30));
509    }
510
511    #[test]
512    fn test_rectangles_adjacent() {
513        let a = Rect::new(0, 0, 10, 10);
514
515        // Horizontally adjacent
516        assert!(rectangles_adjacent(&a, &Rect::new(10, 0, 10, 10)));
517        assert!(rectangles_adjacent(&a, &Rect::new(10, 5, 10, 5))); // Partial overlap on Y
518
519        // Vertically adjacent
520        assert!(rectangles_adjacent(&a, &Rect::new(0, 10, 10, 10)));
521        assert!(rectangles_adjacent(&a, &Rect::new(5, 10, 5, 10))); // Partial overlap on X
522
523        // Not adjacent (gap)
524        assert!(!rectangles_adjacent(&a, &Rect::new(11, 0, 10, 10)));
525
526        // Overlapping (not adjacent)
527        assert!(!rectangles_adjacent(&a, &Rect::new(5, 5, 10, 10)));
528    }
529
530    #[test]
531    fn test_coalesce_regions() {
532        let mut regions = vec![
533            Rect::new(0, 0, 10, 10),
534            Rect::new(10, 0, 10, 10), // Adjacent
535            Rect::new(5, 5, 5, 5),    // Overlapping with first
536        ];
537
538        let original_count = regions.len();
539        let merged = coalesce_regions(&mut regions, 2.0);
540
541        assert!(regions.len() < original_count);
542        assert!(merged > 0);
543    }
544
545    #[test]
546    fn test_topological_sort() {
547        let mut deps = HashMap::new();
548        deps.insert(
549            "C".to_string(),
550            ["A", "B"].iter().map(|s| s.to_string()).collect(),
551        );
552        deps.insert(
553            "B".to_string(),
554            ["A"].iter().map(|s| s.to_string()).collect(),
555        );
556        deps.insert("A".to_string(), HashSet::new());
557
558        let sorted = topological_sort(&deps).unwrap();
559
560        // A should come before B, B should come before C
561        let pos_a = sorted.iter().position(|x| x == "A").unwrap();
562        let pos_b = sorted.iter().position(|x| x == "B").unwrap();
563        let pos_c = sorted.iter().position(|x| x == "C").unwrap();
564
565        assert!(pos_a < pos_b);
566        assert!(pos_b < pos_c);
567    }
568
569    #[test]
570    fn test_topological_sort_cycle() {
571        let mut deps = HashMap::new();
572        deps.insert(
573            "A".to_string(),
574            ["B"].iter().map(|s| s.to_string()).collect(),
575        );
576        deps.insert(
577            "B".to_string(),
578            ["A"].iter().map(|s| s.to_string()).collect(),
579        );
580
581        let result = topological_sort(&deps);
582        assert!(result.is_none()); // Should detect cycle
583    }
584
585    #[test]
586    fn test_fnv1a_hash() {
587        let hash1 = fnv1a_hash(b"hello");
588        let hash2 = fnv1a_hash(b"hello");
589        let hash3 = fnv1a_hash(b"world");
590
591        assert_eq!(hash1, hash2); // Deterministic
592        assert_ne!(hash1, hash3); // Different inputs (extremely likely)
593
594        // Empty input
595        let empty_hash = fnv1a_hash(b"");
596        assert_ne!(empty_hash, 0); // Should not be zero
597    }
598}