Skip to main content

long_screen/kernels/regions/
mod.rs

1//! Region construction (`src/core/layers.ts::LayerLearner.finish` and `RegionAtlas`'s pixel-labelling
2//! constructor), ported stage by stage from the frozen TS oracle in tests/support/reference/layers.ts. Per-frame
3//! accumulation (rust/long-screen/src/kernels/layers.rs) already runs here; this module turns the accumulators the adapter reads
4//! back into `Region`s, once per run, and then labels every native pixel by first-containing-region-wins.
5//!
6//! f64 throughout (JS numbers are f64); `js_round`/`js_floor`/`js_ceil` (geometry.rs) reproduce `Math.round` /
7//! `Math.floor` / `Math.ceil` exactly, including negative and half-integer inputs. Group/label construction walks
8//! cells in the same order a JS `Map`/`Array` insertion order would (see `cells::group_and_label`), because
9//! later stages in the TS compare region *identity*; this port replaces that with a stable index into `regions`.
10//!
11//! One file per construction stage (mirroring the frozen oracle's stage comments): `bands` (stationary edges +
12//! texture test), `cells` (union-find + grouping), `construct` (per-region mask painting, pane divider, band
13//! expansion, cell granularity), `crops` (native-precision edges and crop/solid), `merge` (sliver/sticky-header
14//! cleanup and the final filter), `atlas` (`RegionAtlas` pixel labelling). `finish()`, the top-level orchestrator,
15//! and the shared types/helpers stay here.
16
17use crate::kernels::geometry::Rect;
18
19mod atlas;
20mod band_fn;
21mod bands;
22mod cells;
23mod construct;
24mod crops;
25mod merge;
26#[cfg(test)]
27mod tests;
28
29pub use atlas::{label_atlas, AtlasMask, AtlasRegion};
30use band_fn::band;
31use bands::{detect_bands, strongest_cut, Bands};
32use cells::{group_and_label, union_find_cells};
33use construct::{apply_cell_granularity, apply_pane_divider, build_regions, expand_bands};
34use crops::{
35    assign_native_crops, compute_native_edges, recompute_bounding_boxes, refine_vertical_cut_crops,
36};
37use merge::{cleanup_sticky_headers, finalize, merge_small_regions};
38
39/// Borrowed RGBA8 image (`RGBA` in types.ts), used only for texture/appearance evidence.
40pub struct Rgba<'a> {
41    pub width: usize,
42    pub height: usize,
43    pub data: &'a [u8],
44}
45
46/// The accumulator arrays `LearnerHandle.read()` returns, plus the sizes and retained native frame `finish()`
47/// needs. Mirrors `tests/support/reference/layers.ts::FinishAccumulators`.
48pub struct FinishInput<'a> {
49    pub width: usize,
50    pub height: usize,
51    pub cell: usize,
52    pub informative_frames: f64,
53    pub native_frames: f64,
54    pub row_change: &'a [f64],
55    pub col_change: &'a [f64],
56    /// `Learner::col_moving`: per column, the informative frames in which it moved.
57    pub col_moving: &'a [f64],
58    pub col_mean: &'a [f64],
59    pub col_gain: &'a [f64],
60    pub horizontal_gain: &'a [f64],
61    pub split: &'a [f64],
62    pub evidence: &'a [f64],
63    pub activity: &'a [f64],
64    pub observations: &'a [f64],
65    pub native_row_change: Option<&'a [f64]>,
66    pub native_col_change: Option<&'a [f64]>,
67    pub reference: Option<Rgba<'a>>,
68}
69
70#[derive(Clone, Copy, PartialEq, Eq, Debug)]
71pub enum Kind {
72    Moving,
73    Fixed,
74    Ignore,
75}
76
77/// Constructed region, the Rust analogue of `Region` (types.ts). `mask` is always analysis-resolution
78/// (`mask_width × mask_height`); `RegionAtlas.labels` (`src/core/layers.ts`) is built by
79/// `regions::atlas::label_atlas` over `region::Region<&[u8]>` (region.rs's generic `Region`, instantiated with a
80/// borrowed mask — see `regions/atlas.rs`'s `AtlasRegion`), constructed directly from the wire request in
81/// `abi::regions::ls_regions_label_atlas`. It never goes through this type.
82pub struct RegionOut {
83    pub id: String,
84    pub name: String,
85    pub kind: Kind,
86    pub rect: Rect,
87    pub cells: Option<Vec<i64>>,
88    pub mask: Option<Vec<u8>>,
89    pub mask_width: usize,
90    pub mask_height: usize,
91    pub manual: bool,
92    pub unassigned: bool,
93    pub exclusions: Vec<Rect>,
94    pub crop: Option<Rect>,
95    pub solid: bool,
96    pub factor: u32,
97    /// Construction-only annotation (`Region & { bandSide?: number }` in the TS `finish()`): which stationary
98    /// edge band (1=top, 2=bottom, 3=left, 4=right) `expand_bands` last assigned this region to. A region whose
99    /// cells touch more than one band keeps only the LAST band `expand_bands` visits (1, 2, 3, then 4) —
100    /// reproduced here, not "fixed", because `native_crops` reads it the same way.
101    pub band_side: Option<u8>,
102}
103
104#[inline]
105fn intersect(a: Rect, b: Rect) -> Rect {
106    let x = a.x.max(b.x);
107    let y = a.y.max(b.y);
108    Rect {
109        x,
110        y,
111        width: ((a.x + a.width).min(b.x + b.width) - x).max(0.0),
112        height: ((a.y + a.height).min(b.y + b.height) - y).max(0.0),
113    }
114}
115
116/// A read past the end of a JS `Float64Array` returns `undefined`, which poisons downstream arithmetic to `NaN`
117/// and makes every comparison below false; this reproduces that without ever indexing out of range.
118#[inline]
119fn stat_at(stats: &[f64], index: i64) -> f64 {
120    if index < 0 || index as usize >= stats.len() {
121        f64::NAN
122    } else {
123        stats[index as usize]
124    }
125}
126
127/// Orchestrates every stage above, mirroring `tests/support/reference/layers.ts::referenceFinish` /
128/// `src/core/layers.ts::LayerLearner.finish()` exactly (same stage order, same accumulator reads). The manual-
129/// region branch of `finish()` (`LayerLearner.finish`'s `manual` parameter) is NOT ported here: it is cheap TS
130/// object construction over data the caller already holds (spreading the input `Region`s, no per-pixel work
131/// except the one native-pixel coverage scan, which IS in Rust — `ls_regions_manual_uncovered`,
132/// rust/wasm/src/regions.rs). Porting it would mean Rust re-deriving id/name/exclusions only for TS to reconstruct the
133/// same values back; this function therefore only ever builds the algorithmic-path regions.
134pub fn finish(
135    input: &FinishInput<'_>,
136    native_width: f64,
137    native_height: f64,
138    factor: u32,
139) -> Vec<RegionOut> {
140    let width = input.width as i64;
141    let height = input.height as i64;
142    let cell = input.cell as i64;
143    let cols = (width + cell - 1) / cell.max(1);
144    let rows = (height + cell - 1) / cell.max(1);
145    let reference = input.reference.as_ref();
146    let bands = detect_bands(
147        width,
148        height,
149        input.informative_frames,
150        input.row_change,
151        input.col_moving,
152        input.col_mean,
153        reference,
154        native_width,
155        native_height,
156        factor as f64,
157    );
158    let vertical_cut = strongest_cut(input.col_gain, input.informative_frames);
159    let horizontal_cut = strongest_cut(input.horizontal_gain, input.informative_frames);
160    let mut ds = union_find_cells(
161        cols,
162        rows,
163        cell,
164        width,
165        height,
166        bands.top,
167        bands.bottom,
168        bands.left,
169        bands.right,
170        vertical_cut,
171        horizontal_cut,
172        input.evidence,
173        input.split,
174        input.informative_frames,
175    );
176    let n = cols * rows;
177    let (large, labels) = group_and_label(&mut ds, n, cols);
178    let mut regions = build_regions(
179        &large,
180        &labels,
181        cols,
182        cell,
183        width,
184        height,
185        bands.top,
186        bands.bottom,
187        bands.left,
188        bands.right,
189        input.activity,
190        input.observations,
191        input.informative_frames,
192        factor,
193    );
194    let (divider_start, divider_end) = apply_pane_divider(
195        &mut regions,
196        &labels,
197        cols,
198        rows,
199        cell,
200        width,
201        height,
202        bands.top,
203        bands.bottom,
204        vertical_cut,
205        input.col_change,
206        input.informative_frames,
207        factor,
208    );
209    expand_bands(
210        &mut regions,
211        width,
212        height,
213        bands.top,
214        bands.bottom,
215        bands.left,
216        bands.right,
217        native_width,
218        native_height,
219        cols,
220        cell,
221        factor,
222    );
223    apply_cell_granularity(
224        &mut regions,
225        width,
226        height,
227        cell,
228        bands.top,
229        bands.bottom,
230        bands.left,
231        bands.right,
232    );
233    let edges = compute_native_edges(
234        &bands,
235        input.native_row_change,
236        input.native_col_change,
237        input.native_frames,
238        height,
239        width,
240        factor as f64,
241        native_width,
242        native_height,
243    );
244    assign_native_crops(
245        &mut regions,
246        &edges,
247        native_width,
248        native_height,
249        cols,
250        cell,
251        width,
252        height,
253        bands.top,
254        bands.bottom,
255        bands.left,
256        bands.right,
257    );
258    refine_vertical_cut_crops(
259        &mut regions,
260        &edges,
261        vertical_cut,
262        cell,
263        cols,
264        divider_start,
265        divider_end,
266        input.native_col_change,
267        input.native_frames,
268        native_width,
269        factor as f64,
270    );
271    recompute_bounding_boxes(
272        &mut regions,
273        width,
274        height,
275        native_width,
276        native_height,
277        factor as f64,
278    );
279    let valid: Vec<usize> = (0..regions.len())
280        .filter(|&i| regions[i].rect.width > 0.0 && regions[i].rect.height > 0.0)
281        .collect();
282    merge_small_regions(&mut regions, &valid, factor as f64);
283    cleanup_sticky_headers(
284        &mut regions,
285        &valid,
286        edges.content,
287        cell,
288        factor as f64,
289        reference,
290    );
291    finalize(regions, &valid, edges.content)
292}