Skip to main content

long_screen/kernels/regions/
atlas.rs

1//! RegionAtlas pixel labelling (`src/core/layers.ts::RegionAtlas`'s constructor): native-resolution pixel
2//! ownership, first-containing-region-wins, plus the per-code pixel counts `RegionAtlas.count()` needs — computed
3//! in the same pass so the adapter never scans the label plane a second time in TS.
4//!
5//! `AtlasRegion`/`AtlasMask` are `crate::kernels::region::Region`/`Mask` instantiated over borrowed mask bytes (`&'a [u8]`)
6//! instead of the default `Vec<u8>` — they only need to live for this one `label_atlas` call, unlike
7//! `voting.rs`'s Ring, which retains a region across many calls and must own its mask. Membership logic (Region's
8//! `contains()`) is shared, not mirrored — one implementation, not two independent copies;
9//! `tests/unit/parity/regions.test.ts` still exercises both instantiations through the ABI.
10
11use crate::kernels::geometry::{js_ceil, js_floor};
12use crate::kernels::region::{Mask, Region};
13
14pub type AtlasMask<'a> = Mask<&'a [u8]>;
15pub type AtlasRegion<'a> = Region<&'a [u8]>;
16
17/// Fills `out` (`width × height` bytes, adapter-owned — written directly, never allocated here) with
18/// `RegionAtlas.labels`: 0 = owned by no region, otherwise `index + 1` into `regions`. Returns the per-code pixel
19/// counts (`regions.len() + 1` entries, index 0 unused), or `None` when there are more than 254 regions (one
20/// label byte can only encode 254 non-zero codes) — the ABI turns that into a status the adapter surfaces as the
21/// same error `RegionAtlas`'s TS constructor throws (src/core/layers.ts), never a panic across the boundary.
22pub fn label_atlas(
23    regions: &[AtlasRegion],
24    width: usize,
25    height: usize,
26    out: &mut [u8],
27) -> Option<Vec<u32>> {
28    if regions.len() > 254 {
29        return None;
30    }
31    out.fill(0);
32    let mut counts = vec![0u32; regions.len() + 1];
33    for (index, region) in regions.iter().enumerate() {
34        let code = (index + 1) as u8;
35        let r = region.rect;
36        let x0 = 0i64.max(js_floor(r.x) as i64) as usize;
37        let y0 = 0i64.max(js_floor(r.y) as i64) as usize;
38        let x1 = (width as i64).min(js_ceil(r.x + r.width) as i64).max(0) as usize;
39        let y1 = (height as i64).min(js_ceil(r.y + r.height) as i64).max(0) as usize;
40        for y in y0..y1.min(height) {
41            for x in x0..x1.min(width) {
42                let i = y * width + x;
43                if out[i] == 0 && region.contains(x as f64, y as f64, width as f64, height as f64) {
44                    out[i] = code;
45                    counts[code as usize] += 1;
46                }
47            }
48        }
49    }
50    Some(counts)
51}