Skip to main content

agg_rust/
scanline_boolean_algebra.rs

1//! Scanline boolean algebra.
2//!
3//! Port of `agg_scanline_boolean_algebra.h`.
4//! Provides boolean operations (union, intersect, subtract, XOR) on
5//! rasterized shapes stored in scanline storage.
6
7use crate::rasterizer_scanline_aa::{RasterizerScanlineAa, Scanline};
8use crate::scanline_storage_aa::ScanlineStorageAa;
9use crate::scanline_storage_bin::ScanlineStorageBin;
10use crate::scanline_u::ScanlineU8;
11
12/// Boolean operation type.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SBoolOp {
15    Or,
16    And,
17    Xor,
18    XorSaddle,
19    XorAbsDiff,
20    AMinusB,
21    BMinusA,
22}
23
24const COVER_FULL: u32 = 255;
25const COVER_SHIFT: u32 = 8;
26
27// ============================================================================
28// AA Coverage combination functions
29// ============================================================================
30
31/// Combine two AA coverage values for intersection (AND).
32/// C++: `cover = c1 * c2; if (cover == full*full) full else cover >> shift`
33#[inline]
34fn intersect_covers(c1: u32, c2: u32) -> u8 {
35    let cover = c1 * c2;
36    if cover == COVER_FULL * COVER_FULL {
37        COVER_FULL as u8
38    } else {
39        (cover >> COVER_SHIFT) as u8
40    }
41}
42
43/// Combine two AA coverage values for union (OR).
44/// C++: `cover = full*full - (full-c1)*(full-c2); special case for full*full`
45#[inline]
46fn unite_covers(c1: u32, c2: u32) -> u8 {
47    let cover = COVER_FULL * COVER_FULL - (COVER_FULL - c1) * (COVER_FULL - c2);
48    if cover == COVER_FULL * COVER_FULL {
49        COVER_FULL as u8
50    } else {
51        (cover >> COVER_SHIFT) as u8
52    }
53}
54
55/// Combine two AA coverage values for subtraction (A - B).
56/// C++: `cover = c1 * (full - c2); special case for full*full`
57#[inline]
58fn subtract_covers(c1: u32, c2: u32) -> u8 {
59    let cover = c1 * (COVER_FULL - c2);
60    if cover == COVER_FULL * COVER_FULL {
61        COVER_FULL as u8
62    } else {
63        (cover >> COVER_SHIFT) as u8
64    }
65}
66
67/// Combine two AA coverage values for XOR (linear formula).
68/// C++: `cover = a + b; if (cover > full) cover = full + full - cover`
69#[inline]
70fn xor_covers_linear(c1: u32, c2: u32) -> u8 {
71    let cover = c1 + c2;
72    if cover > COVER_FULL {
73        (COVER_FULL + COVER_FULL - cover) as u8
74    } else {
75        cover as u8
76    }
77}
78
79/// Combine two AA coverage values for XOR (saddle formula).
80/// C++: `1-((1-a+a*b)*(1-b+a*b))` in fixed-point.
81/// a XOR b: cover_mask - ((a' * b') >> cover_shift)
82/// where a' = (mask*mask - (a << shift) + k) >> shift
83///       b' = (mask*mask - (b << shift) + k) >> shift
84///       k  = a * b
85#[inline]
86fn xor_covers_saddle(c1: u32, c2: u32) -> u8 {
87    let k = c1 * c2;
88    if k == COVER_FULL * COVER_FULL {
89        return 0;
90    }
91    let a = (COVER_FULL * COVER_FULL - (c1 << COVER_SHIFT) + k) >> COVER_SHIFT;
92    let b = (COVER_FULL * COVER_FULL - (c2 << COVER_SHIFT) + k) >> COVER_SHIFT;
93    (COVER_FULL - ((a * b) >> COVER_SHIFT)) as u8
94}
95
96/// Combine two AA coverage values for XOR (absolute difference formula).
97/// C++: `abs(a - b)`
98#[inline]
99fn xor_covers_abs_diff(c1: u32, c2: u32) -> u8 {
100    (c1 as i32 - c2 as i32).unsigned_abs() as u8
101}
102
103// ============================================================================
104// Shape-level boolean operations (AA)
105// ============================================================================
106
107/// Perform a boolean operation on two rasterized shapes.
108///
109/// Takes two rasterizers, rasterizes both into storage, then combines them.
110pub fn sbool_combine_shapes_aa(
111    op: SBoolOp,
112    ras1: &mut RasterizerScanlineAa,
113    ras2: &mut RasterizerScanlineAa,
114    sl1: &mut ScanlineU8,
115    sl2: &mut ScanlineU8,
116    sl_result: &mut ScanlineU8,
117    storage1: &mut ScanlineStorageAa,
118    storage2: &mut ScanlineStorageAa,
119    storage_result: &mut ScanlineStorageAa,
120) {
121    // Render shape 1 into storage1
122    storage1.prepare();
123    render_to_storage(ras1, sl1, storage1);
124
125    // Render shape 2 into storage2
126    storage2.prepare();
127    render_to_storage(ras2, sl2, storage2);
128
129    // Combine
130    storage_result.prepare();
131    sbool_combine_storages_aa(op, storage1, storage2, sl_result, storage_result);
132}
133
134/// Render a rasterizer's output into AA storage.
135fn render_to_storage(
136    ras: &mut RasterizerScanlineAa,
137    sl: &mut ScanlineU8,
138    storage: &mut ScanlineStorageAa,
139) {
140    if ras.rewind_scanlines() {
141        sl.reset(ras.min_x(), ras.max_x());
142        while ras.sweep_scanline(sl) {
143            storage.render_scanline_u8(sl);
144        }
145    }
146}
147
148/// Combine two AA scanline storages.
149pub fn sbool_combine_storages_aa(
150    op: SBoolOp,
151    storage1: &ScanlineStorageAa,
152    storage2: &ScanlineStorageAa,
153    sl: &mut ScanlineU8,
154    result: &mut ScanlineStorageAa,
155) {
156    let n1 = storage1.num_scanlines();
157    let n2 = storage2.num_scanlines();
158
159    if n1 == 0 && n2 == 0 {
160        return;
161    }
162
163    // Determine the combined X range for the result scanline
164    let min_x = if n1 > 0 && n2 > 0 {
165        storage1.min_x().min(storage2.min_x())
166    } else if n1 > 0 {
167        storage1.min_x()
168    } else {
169        storage2.min_x()
170    };
171    let max_x = if n1 > 0 && n2 > 0 {
172        storage1.max_x().max(storage2.max_x())
173    } else if n1 > 0 {
174        storage1.max_x()
175    } else {
176        storage2.max_x()
177    };
178
179    // Handle cases where one storage is empty
180    match op {
181        SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff => {
182            // Union/XOR: one empty → result is the other
183            if n1 == 0 {
184                copy_storage_aa(storage2, sl, result, min_x, max_x);
185                return;
186            }
187            if n2 == 0 {
188                copy_storage_aa(storage1, sl, result, min_x, max_x);
189                return;
190            }
191        }
192        SBoolOp::And => {
193            // Intersection: one empty → empty result
194            if n1 == 0 || n2 == 0 {
195                return;
196            }
197        }
198        SBoolOp::AMinusB => {
199            if n1 == 0 {
200                return; // nothing to subtract from
201            }
202            if n2 == 0 {
203                copy_storage_aa(storage1, sl, result, min_x, max_x);
204                return;
205            }
206        }
207        SBoolOp::BMinusA => {
208            if n2 == 0 {
209                return;
210            }
211            if n1 == 0 {
212                copy_storage_aa(storage2, sl, result, min_x, max_x);
213                return;
214            }
215        }
216    }
217
218    // Both storages have scanlines — synchronize Y coordinates
219    let mut i1 = 0usize;
220    let mut i2 = 0usize;
221
222    while i1 < n1 || i2 < n2 {
223        if i1 >= n1 {
224            // Only storage2 remaining
225            match op {
226                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::BMinusA => {
227                    emit_scanline_from_storage(storage2, i2, sl, result, min_x, max_x);
228                }
229                _ => {} // And, AMinusB: nothing
230            }
231            i2 += 1;
232            continue;
233        }
234        if i2 >= n2 {
235            // Only storage1 remaining
236            match op {
237                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::AMinusB => {
238                    emit_scanline_from_storage(storage1, i1, sl, result, min_x, max_x);
239                }
240                _ => {} // And, BMinusA: nothing
241            }
242            i1 += 1;
243            continue;
244        }
245
246        let y1 = storage1.scanline_y(i1);
247        let y2 = storage2.scanline_y(i2);
248
249        if y1 < y2 {
250            // Scanline only in storage1
251            match op {
252                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::AMinusB => {
253                    emit_scanline_from_storage(storage1, i1, sl, result, min_x, max_x);
254                }
255                _ => {}
256            }
257            i1 += 1;
258        } else if y2 < y1 {
259            // Scanline only in storage2
260            match op {
261                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::BMinusA => {
262                    emit_scanline_from_storage(storage2, i2, sl, result, min_x, max_x);
263                }
264                _ => {}
265            }
266            i2 += 1;
267        } else {
268            // Same Y — combine the two scanlines
269            combine_scanlines_aa(op, storage1, i1, storage2, i2, sl, result, min_x, max_x);
270            i1 += 1;
271            i2 += 1;
272        }
273    }
274}
275
276/// Copy all scanlines from a storage into the result.
277fn copy_storage_aa(
278    src: &ScanlineStorageAa,
279    sl: &mut ScanlineU8,
280    result: &mut ScanlineStorageAa,
281    min_x: i32,
282    max_x: i32,
283) {
284    for i in 0..src.num_scanlines() {
285        emit_scanline_from_storage(src, i, sl, result, min_x, max_x);
286    }
287}
288
289/// Emit a single scanline from storage into the result.
290fn emit_scanline_from_storage(
291    src: &ScanlineStorageAa,
292    sl_idx: usize,
293    sl: &mut ScanlineU8,
294    result: &mut ScanlineStorageAa,
295    min_x: i32,
296    max_x: i32,
297) {
298    let y = src.scanline_y(sl_idx);
299    sl.reset(min_x, max_x);
300    sl.reset_spans();
301
302    for sp in src.embedded_spans(sl_idx) {
303        if sp.len < 0 {
304            // Solid span
305            sl.add_span(sp.x, (-sp.len) as u32, sp.covers[0] as u32);
306        } else {
307            for j in 0..sp.len as usize {
308                sl.add_cell(sp.x + j as i32, sp.covers[j] as u32);
309            }
310        }
311    }
312    sl.finalize(y);
313    if sl.num_spans() > 0 {
314        result.render_scanline_u8(sl);
315    }
316}
317
318/// Combine two scanlines at the same Y coordinate.
319fn combine_scanlines_aa(
320    op: SBoolOp,
321    storage1: &ScanlineStorageAa,
322    sl_idx1: usize,
323    storage2: &ScanlineStorageAa,
324    sl_idx2: usize,
325    sl: &mut ScanlineU8,
326    result: &mut ScanlineStorageAa,
327    min_x: i32,
328    max_x: i32,
329) {
330    let y = storage1.scanline_y(sl_idx1);
331    sl.reset(min_x, max_x);
332    sl.reset_spans();
333
334    // Collect spans from both scanlines into flat coverage arrays, then combine.
335    // This is simpler than the C++ span-walking approach and correct for all ops.
336    let width = (max_x - min_x + 1) as usize;
337    let mut cov1 = vec![0u8; width];
338    let mut cov2 = vec![0u8; width];
339
340    // Fill coverage array 1
341    for sp in storage1.embedded_spans(sl_idx1) {
342        let abs_len = sp.abs_len();
343        for j in 0..abs_len {
344            let x = sp.x + j;
345            if x >= min_x && x <= max_x {
346                cov1[(x - min_x) as usize] = sp.cover_at(j as usize);
347            }
348        }
349    }
350
351    // Fill coverage array 2
352    for sp in storage2.embedded_spans(sl_idx2) {
353        let abs_len = sp.abs_len();
354        for j in 0..abs_len {
355            let x = sp.x + j;
356            if x >= min_x && x <= max_x {
357                cov2[(x - min_x) as usize] = sp.cover_at(j as usize);
358            }
359        }
360    }
361
362    // Combine and emit
363    for i in 0..width {
364        let c1 = cov1[i] as u32;
365        let c2 = cov2[i] as u32;
366        let result_cover = match op {
367            SBoolOp::Or => {
368                if c1 > 0 && c2 > 0 {
369                    unite_covers(c1, c2)
370                } else if c1 > 0 {
371                    c1 as u8
372                } else {
373                    c2 as u8
374                }
375            }
376            SBoolOp::And => intersect_covers(c1, c2),
377            SBoolOp::Xor => {
378                if c1 > 0 && c2 > 0 {
379                    xor_covers_linear(c1, c2)
380                } else if c1 > 0 {
381                    c1 as u8
382                } else {
383                    c2 as u8
384                }
385            }
386            SBoolOp::XorSaddle => {
387                if c1 > 0 && c2 > 0 {
388                    xor_covers_saddle(c1, c2)
389                } else if c1 > 0 {
390                    c1 as u8
391                } else {
392                    c2 as u8
393                }
394            }
395            SBoolOp::XorAbsDiff => {
396                if c1 > 0 && c2 > 0 {
397                    xor_covers_abs_diff(c1, c2)
398                } else if c1 > 0 {
399                    c1 as u8
400                } else {
401                    c2 as u8
402                }
403            }
404            SBoolOp::AMinusB => subtract_covers(c1, c2),
405            SBoolOp::BMinusA => subtract_covers(c2, c1),
406        };
407        if result_cover > 0 {
408            sl.add_cell(min_x + i as i32, result_cover as u32);
409        }
410    }
411
412    sl.finalize(y);
413    if sl.num_spans() > 0 {
414        result.render_scanline_u8(sl);
415    }
416}
417
418// ============================================================================
419// Shape-level boolean operations (Binary)
420// ============================================================================
421
422/// Perform a boolean operation on two rasterized shapes (binary/no AA).
423pub fn sbool_combine_shapes_bin(
424    op: SBoolOp,
425    ras1: &mut RasterizerScanlineAa,
426    ras2: &mut RasterizerScanlineAa,
427    sl1: &mut ScanlineU8,
428    sl2: &mut ScanlineU8,
429    sl_result: &mut ScanlineU8,
430    storage1: &mut ScanlineStorageBin,
431    storage2: &mut ScanlineStorageBin,
432    storage_result: &mut ScanlineStorageBin,
433) {
434    // Render shape 1 into storage1
435    storage1.prepare();
436    render_to_bin_storage(ras1, sl1, storage1);
437
438    // Render shape 2 into storage2
439    storage2.prepare();
440    render_to_bin_storage(ras2, sl2, storage2);
441
442    // Combine
443    storage_result.prepare();
444    sbool_combine_storages_bin(op, storage1, storage2, sl_result, storage_result);
445}
446
447/// Render a rasterizer's output into binary storage.
448fn render_to_bin_storage(
449    ras: &mut RasterizerScanlineAa,
450    sl: &mut ScanlineU8,
451    storage: &mut ScanlineStorageBin,
452) {
453    if ras.rewind_scanlines() {
454        sl.reset(ras.min_x(), ras.max_x());
455        while ras.sweep_scanline(sl) {
456            storage.render_scanline_u8(sl);
457        }
458    }
459}
460
461/// Combine two binary scanline storages.
462pub fn sbool_combine_storages_bin(
463    op: SBoolOp,
464    storage1: &ScanlineStorageBin,
465    storage2: &ScanlineStorageBin,
466    sl: &mut ScanlineU8,
467    result: &mut ScanlineStorageBin,
468) {
469    let n1 = storage1.num_scanlines();
470    let n2 = storage2.num_scanlines();
471
472    if n1 == 0 && n2 == 0 {
473        return;
474    }
475
476    let min_x = if n1 > 0 && n2 > 0 {
477        storage1.min_x().min(storage2.min_x())
478    } else if n1 > 0 {
479        storage1.min_x()
480    } else {
481        storage2.min_x()
482    };
483    let max_x = if n1 > 0 && n2 > 0 {
484        storage1.max_x().max(storage2.max_x())
485    } else if n1 > 0 {
486        storage1.max_x()
487    } else {
488        storage2.max_x()
489    };
490
491    match op {
492        SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff => {
493            if n1 == 0 {
494                copy_storage_bin(storage2, sl, result, min_x, max_x);
495                return;
496            }
497            if n2 == 0 {
498                copy_storage_bin(storage1, sl, result, min_x, max_x);
499                return;
500            }
501        }
502        SBoolOp::And => {
503            if n1 == 0 || n2 == 0 {
504                return;
505            }
506        }
507        SBoolOp::AMinusB => {
508            if n1 == 0 {
509                return;
510            }
511            if n2 == 0 {
512                copy_storage_bin(storage1, sl, result, min_x, max_x);
513                return;
514            }
515        }
516        SBoolOp::BMinusA => {
517            if n2 == 0 {
518                return;
519            }
520            if n1 == 0 {
521                copy_storage_bin(storage2, sl, result, min_x, max_x);
522                return;
523            }
524        }
525    }
526
527    let mut i1 = 0usize;
528    let mut i2 = 0usize;
529
530    while i1 < n1 || i2 < n2 {
531        if i1 >= n1 {
532            match op {
533                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::BMinusA => {
534                    emit_scanline_from_bin_storage(storage2, i2, sl, result, min_x, max_x);
535                }
536                _ => {}
537            }
538            i2 += 1;
539            continue;
540        }
541        if i2 >= n2 {
542            match op {
543                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::AMinusB => {
544                    emit_scanline_from_bin_storage(storage1, i1, sl, result, min_x, max_x);
545                }
546                _ => {}
547            }
548            i1 += 1;
549            continue;
550        }
551
552        let y1 = storage1.scanline_y(i1);
553        let y2 = storage2.scanline_y(i2);
554
555        if y1 < y2 {
556            match op {
557                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::AMinusB => {
558                    emit_scanline_from_bin_storage(storage1, i1, sl, result, min_x, max_x);
559                }
560                _ => {}
561            }
562            i1 += 1;
563        } else if y2 < y1 {
564            match op {
565                SBoolOp::Or | SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff | SBoolOp::BMinusA => {
566                    emit_scanline_from_bin_storage(storage2, i2, sl, result, min_x, max_x);
567                }
568                _ => {}
569            }
570            i2 += 1;
571        } else {
572            combine_scanlines_bin(op, storage1, i1, storage2, i2, sl, result, min_x, max_x);
573            i1 += 1;
574            i2 += 1;
575        }
576    }
577}
578
579fn copy_storage_bin(
580    src: &ScanlineStorageBin,
581    sl: &mut ScanlineU8,
582    result: &mut ScanlineStorageBin,
583    min_x: i32,
584    max_x: i32,
585) {
586    // Re-emit via ScanlineBin
587    let mut sl_bin = crate::scanline_bin::ScanlineBin::new();
588    for i in 0..src.num_scanlines() {
589        let y = src.scanline_y(i);
590        sl_bin.reset(min_x, max_x);
591        sl_bin.reset_spans();
592        for sp in src.embedded_spans(i) {
593            sl_bin.add_span(sp.x, sp.len as u32, COVER_FULL);
594        }
595        sl_bin.finalize(y);
596        if sl_bin.num_spans() > 0 {
597            result.render_scanline_bin(&sl_bin);
598        }
599    }
600    let _ = sl; // unused but kept for API consistency
601}
602
603fn emit_scanline_from_bin_storage(
604    src: &ScanlineStorageBin,
605    sl_idx: usize,
606    sl: &mut ScanlineU8,
607    result: &mut ScanlineStorageBin,
608    min_x: i32,
609    max_x: i32,
610) {
611    let y = src.scanline_y(sl_idx);
612    let mut sl_bin = crate::scanline_bin::ScanlineBin::new();
613    sl_bin.reset(min_x, max_x);
614    sl_bin.reset_spans();
615    for sp in src.embedded_spans(sl_idx) {
616        sl_bin.add_span(sp.x, sp.len as u32, COVER_FULL);
617    }
618    sl_bin.finalize(y);
619    if sl_bin.num_spans() > 0 {
620        result.render_scanline_bin(&sl_bin);
621    }
622    let _ = sl; // unused but kept for API consistency
623}
624
625fn combine_scanlines_bin(
626    op: SBoolOp,
627    storage1: &ScanlineStorageBin,
628    sl_idx1: usize,
629    storage2: &ScanlineStorageBin,
630    sl_idx2: usize,
631    sl: &mut ScanlineU8,
632    result: &mut ScanlineStorageBin,
633    min_x: i32,
634    max_x: i32,
635) {
636    let y = storage1.scanline_y(sl_idx1);
637    let width = (max_x - min_x + 1) as usize;
638    let mut bits1 = vec![false; width];
639    let mut bits2 = vec![false; width];
640
641    // Fill bitmask 1
642    for sp in storage1.embedded_spans(sl_idx1) {
643        for j in 0..sp.len {
644            let x = sp.x + j;
645            if x >= min_x && x <= max_x {
646                bits1[(x - min_x) as usize] = true;
647            }
648        }
649    }
650
651    // Fill bitmask 2
652    for sp in storage2.embedded_spans(sl_idx2) {
653        for j in 0..sp.len {
654            let x = sp.x + j;
655            if x >= min_x && x <= max_x {
656                bits2[(x - min_x) as usize] = true;
657            }
658        }
659    }
660
661    // Combine and emit
662    let mut sl_bin = crate::scanline_bin::ScanlineBin::new();
663    sl_bin.reset(min_x, max_x);
664    sl_bin.reset_spans();
665
666    for i in 0..width {
667        let in1 = bits1[i];
668        let in2 = bits2[i];
669        let result_on = match op {
670            SBoolOp::Or => in1 || in2,
671            SBoolOp::And => in1 && in2,
672            SBoolOp::Xor | SBoolOp::XorSaddle | SBoolOp::XorAbsDiff => in1 ^ in2,
673            SBoolOp::AMinusB => in1 && !in2,
674            SBoolOp::BMinusA => in2 && !in1,
675        };
676        if result_on {
677            sl_bin.add_cell(min_x + i as i32, COVER_FULL);
678        }
679    }
680
681    sl_bin.finalize(y);
682    if sl_bin.num_spans() > 0 {
683        result.render_scanline_bin(&sl_bin);
684    }
685    let _ = sl; // API consistency
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691
692    // Helper: create AA storage with a horizontal band from x1..x2 at rows y1..y2
693    fn make_aa_rect(
694        x1: i32,
695        x2: i32,
696        y1: i32,
697        y2: i32,
698        cover: u8,
699    ) -> ScanlineStorageAa {
700        let mut storage = ScanlineStorageAa::new();
701        storage.prepare();
702        let mut sl = ScanlineU8::new();
703        for y in y1..=y2 {
704            sl.reset(x1, x2);
705            sl.reset_spans();
706            for x in x1..=x2 {
707                sl.add_cell(x, cover as u32);
708            }
709            sl.finalize(y);
710            storage.render_scanline_u8(&sl);
711        }
712        storage
713    }
714
715    // Helper: create binary storage with a rect
716    fn make_bin_rect(x1: i32, x2: i32, y1: i32, y2: i32) -> ScanlineStorageBin {
717        let mut storage = ScanlineStorageBin::new();
718        storage.prepare();
719        let mut sl = crate::scanline_bin::ScanlineBin::new();
720        for y in y1..=y2 {
721            sl.reset(x1, x2);
722            sl.reset_spans();
723            sl.add_span(x1, (x2 - x1 + 1) as u32, 255);
724            sl.finalize(y);
725            storage.render_scanline_bin(&sl);
726        }
727        storage
728    }
729
730    #[test]
731    fn test_aa_intersection() {
732        // Two overlapping rectangles
733        let s1 = make_aa_rect(0, 19, 0, 19, 255);
734        let s2 = make_aa_rect(10, 29, 10, 29, 255);
735
736        let mut result = ScanlineStorageAa::new();
737        let mut sl = ScanlineU8::new();
738        sbool_combine_storages_aa(SBoolOp::And, &s1, &s2, &mut sl, &mut result);
739
740        // Intersection should be [10..19, 10..19]
741        assert_eq!(result.num_scanlines(), 10);
742        assert_eq!(result.min_x(), 10);
743        assert_eq!(result.max_x(), 19);
744        assert_eq!(result.min_y(), 10);
745        assert_eq!(result.max_y(), 19);
746    }
747
748    #[test]
749    fn test_aa_union() {
750        let s1 = make_aa_rect(0, 9, 0, 4, 255);
751        let s2 = make_aa_rect(5, 14, 0, 4, 255);
752
753        let mut result = ScanlineStorageAa::new();
754        let mut sl = ScanlineU8::new();
755        sbool_combine_storages_aa(SBoolOp::Or, &s1, &s2, &mut sl, &mut result);
756
757        // Union should span [0..14, 0..4]
758        assert_eq!(result.num_scanlines(), 5);
759        assert_eq!(result.min_x(), 0);
760        assert_eq!(result.max_x(), 14);
761    }
762
763    #[test]
764    fn test_aa_subtract() {
765        let s1 = make_aa_rect(0, 19, 0, 19, 255);
766        let s2 = make_aa_rect(10, 29, 10, 29, 255);
767
768        let mut result = ScanlineStorageAa::new();
769        let mut sl = ScanlineU8::new();
770        sbool_combine_storages_aa(SBoolOp::AMinusB, &s1, &s2, &mut sl, &mut result);
771
772        // A-B: scanlines 0..9 fully present, scanlines 10..19 only x=0..9
773        assert_eq!(result.num_scanlines(), 20);
774    }
775
776    #[test]
777    fn test_aa_xor() {
778        let s1 = make_aa_rect(0, 9, 0, 4, 200);
779        let s2 = make_aa_rect(5, 14, 0, 4, 200);
780
781        let mut result = ScanlineStorageAa::new();
782        let mut sl = ScanlineU8::new();
783        sbool_combine_storages_aa(SBoolOp::Xor, &s1, &s2, &mut sl, &mut result);
784
785        // XOR: both have coverage → reduced; only one → full
786        assert_eq!(result.num_scanlines(), 5);
787    }
788
789    #[test]
790    fn test_aa_intersect_no_overlap() {
791        let s1 = make_aa_rect(0, 9, 0, 4, 255);
792        let s2 = make_aa_rect(20, 29, 10, 14, 255);
793
794        let mut result = ScanlineStorageAa::new();
795        let mut sl = ScanlineU8::new();
796        sbool_combine_storages_aa(SBoolOp::And, &s1, &s2, &mut sl, &mut result);
797
798        // No overlap → empty intersection
799        assert_eq!(result.num_scanlines(), 0);
800    }
801
802    #[test]
803    fn test_aa_union_disjoint() {
804        let s1 = make_aa_rect(0, 4, 0, 2, 255);
805        let s2 = make_aa_rect(10, 14, 5, 7, 255);
806
807        let mut result = ScanlineStorageAa::new();
808        let mut sl = ScanlineU8::new();
809        sbool_combine_storages_aa(SBoolOp::Or, &s1, &s2, &mut sl, &mut result);
810
811        // 3 + 3 scanlines (no overlap in Y)
812        assert_eq!(result.num_scanlines(), 6);
813    }
814
815    #[test]
816    fn test_aa_empty_operand() {
817        let s1 = make_aa_rect(0, 9, 0, 4, 255);
818        let s2 = ScanlineStorageAa::new();
819
820        let mut result = ScanlineStorageAa::new();
821        let mut sl = ScanlineU8::new();
822
823        // OR with empty → copy of s1
824        sbool_combine_storages_aa(SBoolOp::Or, &s1, &s2, &mut sl, &mut result);
825        assert_eq!(result.num_scanlines(), 5);
826
827        // AND with empty → empty
828        result.prepare();
829        sbool_combine_storages_aa(SBoolOp::And, &s1, &s2, &mut sl, &mut result);
830        assert_eq!(result.num_scanlines(), 0);
831    }
832
833    #[test]
834    fn test_aa_semi_transparent_intersection() {
835        // Two semi-transparent rectangles
836        let s1 = make_aa_rect(0, 9, 0, 0, 128);
837        let s2 = make_aa_rect(0, 9, 0, 0, 128);
838
839        let mut result = ScanlineStorageAa::new();
840        let mut sl = ScanlineU8::new();
841        sbool_combine_storages_aa(SBoolOp::And, &s1, &s2, &mut sl, &mut result);
842
843        // Intersection of two 128-cover spans → (128*128 + 127) >> 8 = 64
844        assert_eq!(result.num_scanlines(), 1);
845
846        // Read back the coverage
847        assert!(result.rewind_scanlines());
848        let mut sl_out = ScanlineU8::new();
849        sl_out.reset(0, 9);
850        assert!(result.sweep_scanline(&mut sl_out));
851        let spans = sl_out.begin();
852        let covers = sl_out.covers();
853        // Check one pixel's coverage
854        let cover = covers[spans[0].cover_offset];
855        assert_eq!(cover, 64);
856    }
857
858    #[test]
859    fn test_bin_intersection() {
860        let s1 = make_bin_rect(0, 19, 0, 19);
861        let s2 = make_bin_rect(10, 29, 10, 29);
862
863        let mut result = ScanlineStorageBin::new();
864        let mut sl = ScanlineU8::new();
865        sbool_combine_storages_bin(SBoolOp::And, &s1, &s2, &mut sl, &mut result);
866
867        assert_eq!(result.num_scanlines(), 10);
868    }
869
870    #[test]
871    fn test_bin_union() {
872        let s1 = make_bin_rect(0, 4, 0, 2);
873        let s2 = make_bin_rect(10, 14, 5, 7);
874
875        let mut result = ScanlineStorageBin::new();
876        let mut sl = ScanlineU8::new();
877        sbool_combine_storages_bin(SBoolOp::Or, &s1, &s2, &mut sl, &mut result);
878
879        assert_eq!(result.num_scanlines(), 6);
880    }
881
882    #[test]
883    fn test_bin_xor() {
884        let s1 = make_bin_rect(0, 9, 0, 4);
885        let s2 = make_bin_rect(5, 14, 0, 4);
886
887        let mut result = ScanlineStorageBin::new();
888        let mut sl = ScanlineU8::new();
889        sbool_combine_storages_bin(SBoolOp::Xor, &s1, &s2, &mut sl, &mut result);
890
891        assert_eq!(result.num_scanlines(), 5);
892        // Each scanline should have only the non-overlapping parts: [0..4] and [10..14]
893    }
894
895    #[test]
896    fn test_bin_subtract() {
897        let s1 = make_bin_rect(0, 19, 0, 19);
898        let s2 = make_bin_rect(10, 29, 10, 29);
899
900        let mut result = ScanlineStorageBin::new();
901        let mut sl = ScanlineU8::new();
902        sbool_combine_storages_bin(SBoolOp::AMinusB, &s1, &s2, &mut sl, &mut result);
903
904        // All 20 scanlines from s1 should be present (rows 0..19)
905        // rows 0..9: full width, rows 10..19: only x=0..9
906        assert_eq!(result.num_scanlines(), 20);
907    }
908
909    #[test]
910    fn test_cover_math_intersect() {
911        assert_eq!(intersect_covers(255, 255), 255); // full*full → full
912        assert_eq!(intersect_covers(0, 255), 0);
913        assert_eq!(intersect_covers(128, 128), 64); // 16384 >> 8 = 64
914    }
915
916    #[test]
917    fn test_cover_math_unite() {
918        assert_eq!(unite_covers(255, 255), 255);
919        assert_eq!(unite_covers(0, 0), 0);
920        assert_eq!(unite_covers(0, 255), 255); // full*full special case
921        // 128 OR 128: 65025 - 127*127 = 48896, >> 8 = 191
922        assert_eq!(unite_covers(128, 128), 191);
923    }
924
925    #[test]
926    fn test_cover_math_subtract() {
927        assert_eq!(subtract_covers(255, 0), 255); // 255*255 == full*full → 255
928        assert_eq!(subtract_covers(255, 255), 0);
929        assert_eq!(subtract_covers(0, 255), 0);
930    }
931
932    #[test]
933    fn test_cover_math_xor_linear() {
934        assert_eq!(xor_covers_linear(0, 0), 0);
935        assert_eq!(xor_covers_linear(255, 0), 255);
936        assert_eq!(xor_covers_linear(0, 255), 255);
937        assert_eq!(xor_covers_linear(255, 255), 0); // 510 > 255, 255+255-510 = 0
938    }
939
940    #[test]
941    fn test_cover_math_xor_saddle() {
942        // Note: saddle formula has small fixed-point imprecision at (0,0)
943        // returning 3 instead of 0, which is expected and doesn't affect
944        // rendering since the combine functor is only called when both covers > 0.
945        assert!(xor_covers_saddle(0, 0) <= 5); // near-zero
946        assert_eq!(xor_covers_saddle(255, 255), 0);
947        // Saddle formula: 1-((1-a+ab)(1-b+ab)) in [0,255] space
948        // With partial coverage, saddle gives different result than linear
949        let saddle = xor_covers_saddle(128, 128);
950        assert!(saddle > 0 && saddle < 255);
951        // Verify saddle is different from linear for partial coverage
952        let linear = xor_covers_linear(128, 128);
953        assert_ne!(saddle, linear);
954    }
955
956    #[test]
957    fn test_cover_math_xor_abs_diff() {
958        assert_eq!(xor_covers_abs_diff(0, 0), 0);
959        assert_eq!(xor_covers_abs_diff(255, 255), 0);
960        assert_eq!(xor_covers_abs_diff(200, 100), 100);
961        assert_eq!(xor_covers_abs_diff(100, 200), 100);
962    }
963}