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(|(_, °ree)| 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}