Skip to main content

ras_commander_fast_map/
sloping.rs

1//! Experimental sloping reconstruction. Native qualification is stage-specific.
2//!
3//! Topology is immutable; hydraulic connectivity and corner arcs are rebuilt for each
4//! profile. A supplied Rayon pool bounds parallel work without changing summation order.
5//! Raster ownership, polygon weights and final depth delivery are separate stages.
6
7use crate::MapError;
8use rayon::prelude::*;
9use std::sync::atomic::{AtomicBool, Ordering};
10
11/// User-visible RAS Mapper sloping controls. Internal experimental modes are excluded.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
13#[serde(deny_unknown_fields)]
14pub struct Options {
15    #[serde(default)]
16    pub render_mode: RenderMode,
17    #[serde(default)]
18    pub reduce_shallow_to_horizontal: bool,
19    #[serde(default)]
20    pub use_depth_weighted_faces: bool,
21}
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
23pub enum RenderMode {
24    #[default]
25    #[serde(rename = "sloping")]
26    Corners,
27    #[serde(rename = "slopingPretty")]
28    WithFaces,
29}
30impl Options {
31    pub fn validate(self) -> Result<(), MapError> {
32        if self.render_mode == RenderMode::Corners
33            && (self.reduce_shallow_to_horizontal || self.use_depth_weighted_faces)
34        {
35            return Err(MapError::Invalid(
36                "shallow and depth-weighted controls require slopingPretty".into(),
37            ));
38        }
39        Ok(())
40    }
41}
42
43pub const NODATA: f32 = -9999.0;
44
45/// Native uncached face application point: the first centre-line intersection
46/// is accepted only within the middle 90% of the full face polyline.
47pub fn face_application(points: &[[f64; 2]], centers: [[f64; 2]; 2]) -> Result<[f64; 2], MapError> {
48    if points.len() < 2
49        || points
50            .iter()
51            .flatten()
52            .chain(centers.iter().flatten())
53            .any(|v| !v.is_finite())
54    {
55        return Err(MapError::Invalid(
56            "invalid sloping face polyline or centres".into(),
57        ));
58    }
59    let [c, d] = centers;
60    let cx = d[0] - c[0];
61    let cy = d[1] - c[1];
62    let length = |a: [f64; 2], b: [f64; 2]| {
63        let x = b[0] - a[0];
64        let y = b[1] - a[1];
65        (x * x + y * y).sqrt()
66    };
67    let total = points.windows(2).map(|p| length(p[0], p[1])).sum::<f64>();
68    let mut preceding = 0.0;
69    for segment in points.windows(2) {
70        let [a, b] = [segment[0], segment[1]];
71        let before = preceding;
72        let segment_length = length(a, b);
73        preceding += segment_length;
74        if (0..2).any(|axis| {
75            a[axis].max(b[axis]) + 1e-6 < c[axis].min(d[axis])
76                || c[axis].max(d[axis]) + 1e-6 < a[axis].min(b[axis])
77        }) {
78            continue;
79        }
80        let ax = b[0] - a[0];
81        let ay = b[1] - a[1];
82        let determinant = cy * ax - cx * ay;
83        if determinant == 0.0 {
84            continue;
85        }
86        let dy = a[1] - c[1];
87        let dx = a[0] - c[0];
88        let t = (cx * dy - cy * dx) / determinant;
89        let u = (ax * dy - ay * dx) / determinant;
90        if t > -1e-6 && t < 1.0 + 1e-6 && u > -1e-6 && u < 1.0 + 1e-6 {
91            let fraction = (before + t.clamp(0.0, 1.0) * segment_length) / total;
92            if fraction > 0.05 && fraction < 0.95 {
93                // Native returns XY computed before clamping the segment fractions.
94                return Ok([a[0] + t * ax, a[1] + t * ay]);
95            }
96            break;
97        }
98    }
99    let half = total * 0.5;
100    let mut running = 0.0;
101    for segment in points.windows(2) {
102        let before = running;
103        let segment_length = length(segment[0], segment[1]);
104        running += segment_length;
105        if running >= half && segment_length > 0.0 {
106            let t = (half - before) / segment_length;
107            return Ok([
108                segment[0][0] + t * (segment[1][0] - segment[0][0]),
109                segment[0][1] + t * (segment[1][1] - segment[0][1]),
110            ]);
111        }
112    }
113    Ok(*points.last().unwrap())
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub enum Connection {
118    None,
119    Levee,
120    Backfill,
121    DownhillDeep,
122    DownhillIntermediate,
123    DownhillShallow,
124}
125
126impl Connection {
127    pub fn connected(self) -> bool {
128        !matches!(self, Self::None | Self::Levee)
129    }
130}
131
132#[derive(Clone, Copy, Debug)]
133pub struct Cell {
134    pub minimum: f32,
135    pub virtual_cell: bool,
136}
137
138#[derive(Clone, Debug)]
139pub struct Face {
140    pub cells: [usize; 2],
141    pub vertices: [usize; 2],
142    pub minimum: f32,
143    pub perimeter: bool,
144    pub application: [f64; 2],
145}
146
147#[derive(Clone, Debug)]
148pub struct Corner {
149    pub point: [f64; 2],
150    pub faces: Vec<usize>,
151    pub ccw: Vec<usize>,
152    pub cw: Vec<usize>,
153}
154
155#[derive(Clone, Copy, Debug)]
156pub struct FaceWater {
157    pub values: [f32; 2],
158    pub connection: Connection,
159}
160
161/// Oriented native mesh incidence. Face endpoint order is not a spatial sort key.
162pub struct Mesh {
163    pub cells: Vec<Cell>,
164    pub faces: Vec<Face>,
165    pub corners: Vec<Corner>,
166}
167
168pub struct Reconstruction {
169    pub faces: Vec<FaceWater>,
170    pub corners: Vec<Vec<f32>>,
171}
172
173impl Mesh {
174    pub fn validate(&self) -> Result<(), MapError> {
175        if self.cells.is_empty()
176            || self.faces.is_empty()
177            || self.corners.is_empty()
178            || self.cells.iter().any(|c| !c.minimum.is_finite())
179        {
180            return Err(MapError::Invalid(
181                "empty sloping mesh or nonfinite cell minimum".into(),
182            ));
183        }
184        for face in &self.faces {
185            if face.cells.iter().any(|&i| i >= self.cells.len())
186                || face.vertices.iter().any(|&i| i >= self.corners.len())
187                || !face.minimum.is_finite()
188                || face.application.iter().any(|v| !v.is_finite())
189            {
190                return Err(MapError::Invalid(
191                    "sloping face incidence out of range".into(),
192                ));
193            }
194        }
195        for (vertex, corner) in self.corners.iter().enumerate() {
196            let n = corner.faces.len();
197            if corner.ccw.len() != n
198                || corner.cw.len() != n
199                || corner.point.iter().any(|v| !v.is_finite())
200            {
201                return Err(MapError::Invalid(
202                    "sloping corner ring length mismatch".into(),
203                ));
204            }
205            for j in 0..n {
206                let face = self
207                    .faces
208                    .get(corner.faces[j])
209                    .ok_or_else(|| MapError::Invalid("sloping corner face out of range".into()))?;
210                if !face.vertices.contains(&vertex)
211                    || corner.ccw[j] >= n
212                    || corner.cw[j] >= n
213                    || corner.cw[corner.ccw[j]] != j
214                {
215                    return Err(MapError::Invalid(
216                        "invalid sloping corner orientation".into(),
217                    ));
218                }
219            }
220            if n > 0 {
221                let mut j = 0;
222                for step in 0..n {
223                    j = corner.ccw[j];
224                    if (j == 0) != (step + 1 == n) {
225                        return Err(MapError::Invalid(
226                            "sloping incidence is not one ring".into(),
227                        ));
228                    }
229                }
230            }
231        }
232        Ok(())
233    }
234
235    pub fn reconstruct(
236        &self,
237        wse: &[f32],
238        plot_tolerance: f32,
239        pool: &rayon::ThreadPool,
240    ) -> Result<Reconstruction, MapError> {
241        self.validate()?;
242        if wse.len() != self.cells.len()
243            || wse.iter().any(|v| !v.is_finite())
244            || !plot_tolerance.is_finite()
245            || plot_tolerance < 0.0
246        {
247            return Err(MapError::Invalid(
248                "invalid sloping profile or native plot tolerance".into(),
249            ));
250        }
251        pool.install(|| {
252            let faces: Vec<_> = self
253                .faces
254                .par_iter()
255                .map(|face| reconstruct_face(face, &self.cells, wse, plot_tolerance))
256                .collect();
257            let corners = self
258                .corners
259                .par_iter()
260                .enumerate()
261                .map(|(i, corner)| reconstruct_corner(i, corner, &self.faces, &faces))
262                .collect();
263            Ok(Reconstruction { faces, corners })
264        })
265    }
266}
267
268/// Preserve binary32 intermediate rounding and native face classification.
269fn reconstruct_face(face: &Face, cells: &[Cell], wse: &[f32], tolerance: f32) -> FaceWater {
270    let [a, b] = face.cells;
271    let [wa, wb] = [wse[a], wse[b]];
272    let [za, zb] = [cells[a].minimum, cells[b].minimum];
273    let dry_a = wa <= za + tolerance;
274    let dry_b = wb <= zb + tolerance;
275    if dry_a || dry_b || face.perimeter {
276        return FaceWater {
277            values: [
278                if dry_a || cells[a].virtual_cell {
279                    NODATA
280                } else {
281                    wa
282                },
283                if dry_b || cells[b].virtual_cell {
284                    NODATA
285                } else {
286                    wb
287                },
288            ],
289            connection: Connection::None,
290        };
291    }
292    if wa <= face.minimum && wb <= face.minimum {
293        return FaceWater {
294            values: [wa, wb],
295            connection: Connection::None,
296        };
297    }
298    let (upstream, high, low) = if wa > wb { (a, wa, wb) } else { (b, wb, wa) };
299    let floor = cells[upstream].minimum;
300    let elevation_delta = zb - za;
301    let backfill = (wb - wa) * elevation_delta <= 0.0;
302    let relief = elevation_delta.abs();
303    let depth = high - floor;
304    let downstream = face.minimum.max(low);
305    let submerged = downstream - floor;
306    let head = high - face.minimum;
307    let average = (wa + wb) * 0.5;
308    let critical = head * (2.0_f32 / 3.0_f32) + face.minimum;
309    let critical_cap = average.partial_cmp(&critical) != Some(std::cmp::Ordering::Greater);
310    let levee = critical_cap && depth / head > 2.0;
311    // The current adjusted native method retains the upstream WSE here;
312    // the critical computation classifies connectivity, not an averaged level.
313    if levee {
314        return FaceWater {
315            values: if upstream == a {
316                [high, wb]
317            } else {
318                [wa, high]
319            },
320            connection: Connection::Levee,
321        };
322    }
323    if backfill {
324        return FaceWater {
325            values: [high; 2],
326            connection: Connection::Backfill,
327        };
328    }
329    if depth >= 2.0 * relief {
330        return FaceWater {
331            values: [high; 2],
332            connection: Connection::DownhillDeep,
333        };
334    }
335    let mut value = downstream + (depth * depth - submerged * submerged) / (2.0 * relief);
336    let connection = if depth > relief {
337        value = ((2.0 * relief - depth) * value + (depth - relief) * high) / relief;
338        Connection::DownhillIntermediate
339    } else {
340        Connection::DownhillShallow
341    };
342    FaceWater {
343        values: [value; 2],
344        connection,
345    }
346}
347
348#[derive(Default)]
349struct Regression {
350    xx: f64,
351    x: f64,
352    yy: f64,
353    y: f64,
354    z: f64,
355    xy: f64,
356    yz: f64,
357    xz: f64,
358    n: usize,
359}
360
361impl Regression {
362    fn add(&mut self, origin: [f64; 2], point: [f64; 2], value: f32) {
363        let x = point[0] - origin[0];
364        let y = point[1] - origin[1];
365        let z = f64::from(value);
366        self.xx += x * x;
367        self.x += x;
368        self.yy += y * y;
369        self.y += y;
370        self.z += z;
371        self.xy += x * y;
372        self.yz += y * z;
373        self.xz += x * z;
374        self.n += 1;
375    }
376
377    fn solve(&self) -> f32 {
378        if self.n == 1 {
379            return self.z as f32;
380        }
381        if self.n == 2 {
382            return (self.z * 0.5) as f32;
383        }
384        let n = self.n as f64;
385        let determinant = self.xx * (self.yy * n - self.y * self.y)
386            - self.xy * (self.xy * n - self.y * self.x)
387            + self.x * (self.xy * self.y - self.yy * self.x);
388        if determinant == 0.0 {
389            return (self.z / n) as f32;
390        }
391        ((self.xx * (self.yy * self.z - self.yz * self.y)
392            - self.xy * (self.xy * self.z - self.yz * self.x)
393            + self.xz * (self.xy * self.y - self.yy * self.x))
394            / determinant) as f32
395    }
396}
397
398fn reconstruct_corner(
399    vertex: usize,
400    corner: &Corner,
401    faces: &[Face],
402    values: &[FaceWater],
403) -> Vec<f32> {
404    let mut output = vec![NODATA; corner.faces.len()];
405    if corner
406        .faces
407        .iter()
408        .all(|&f| values[f].values == [NODATA; 2])
409    {
410        return output;
411    }
412    for j in 0..corner.faces.len() {
413        if output[j] != NODATA {
414            continue;
415        }
416        let mut stop = corner.ccw[j];
417        while values[corner.faces[stop]].connection.connected() && stop != j {
418            stop = corner.ccw[stop];
419        }
420        let mut first = j;
421        if first != stop {
422            while values[corner.faces[first]].connection.connected() {
423                first = corner.cw[first];
424            }
425        }
426        let mut regression = Regression::default();
427        let mut current = first;
428        loop {
429            let f = corner.faces[current];
430            let side = usize::from(faces[f].vertices[0] != vertex);
431            let value = values[f].values[side];
432            if value != NODATA {
433                regression.add(corner.point, faces[f].application, value);
434            }
435            output[current] = value;
436            current = corner.ccw[current];
437            if current == stop {
438                break;
439            }
440        }
441        let boundary = corner.faces[stop];
442        if !values[boundary].connection.connected() {
443            let side = usize::from(faces[boundary].vertices[0] == vertex);
444            let value = values[boundary].values[side];
445            if value != NODATA {
446                regression.add(corner.point, faces[boundary].application, value);
447            }
448        }
449        if regression.n > 0 {
450            let value = regression.solve();
451            current = first;
452            loop {
453                output[current] = value;
454                current = corner.ccw[current];
455                if current == stop {
456                    break;
457                }
458            }
459        }
460        if first == stop {
461            break;
462        }
463    }
464    output
465}
466
467/// Immutable coefficients for oriented face endpoints, not a centre-point TIN.
468pub struct PolygonStencil {
469    points: Vec<[f64; 2]>,
470    corner_products: Vec<f64>,
471}
472
473/// Reuse one scratch buffer per worker; no per-pixel allocation is necessary.
474pub struct WeightScratch {
475    clipped: bool,
476    cross: Vec<f32>,
477    products: Vec<f64>,
478    weights: Vec<f32>,
479    donated: Vec<f32>,
480    clockwise: Vec<f32>,
481    counterclockwise: Vec<f32>,
482}
483
484fn cross(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
485    let x1 = b[0] - a[0];
486    let y1 = b[1] - a[1];
487    let x2 = c[0] - a[0];
488    let y2 = c[1] - a[1];
489    x1 * y2 - y1 * x2
490}
491
492impl WeightScratch {
493    /// Clears the retained donation scratch (native: every 256 points).
494    pub(crate) fn clear_donation_history(&mut self) {
495        self.clockwise.fill(0.);
496        self.counterclockwise.fill(0.);
497    }
498
499    pub fn new(vertices: usize) -> Self {
500        Self {
501            clipped: false,
502            cross: vec![0.0; vertices],
503            products: vec![0.0; vertices],
504            weights: vec![0.0; vertices],
505            donated: vec![0.0; 2 * vertices],
506            clockwise: vec![0.0; vertices],
507            counterclockwise: vec![0.0; vertices],
508        }
509    }
510
511    fn resize(&mut self, vertices: usize) {
512        self.cross.resize(vertices, 0.0);
513        self.products.resize(vertices, 0.0);
514        self.weights.resize(vertices, 0.0);
515        self.donated.resize(2 * vertices, 0.0);
516        self.clockwise.resize(vertices, 0.0);
517        self.counterclockwise.resize(vertices, 0.0);
518    }
519}
520
521impl PolygonStencil {
522    // The native square test intentionally rounds translated coordinates to f32.
523    fn square_bounds(&self) -> Option<[f64; 4]> {
524        if self.points.len() != 4 {
525            return None;
526        }
527        let min_x = self
528            .points
529            .iter()
530            .map(|p| p[0])
531            .fold(f64::INFINITY, f64::min);
532        let min_y = self
533            .points
534            .iter()
535            .map(|p| p[1])
536            .fold(f64::INFINITY, f64::min);
537        let x: Vec<f32> = self.points.iter().map(|p| (p[0] - min_x) as f32).collect();
538        let y: Vec<f32> = self.points.iter().map(|p| (p[1] - min_y) as f32).collect();
539        let cx = (x[0] + x[1] + x[2] + x[3]) / 4.0;
540        let cy = (y[0] + y[1] + y[2] + y[3]) / 4.0;
541        let radius = (x[0] - cx).abs();
542        if radius == 0.0
543            || (0..4).any(|i| {
544                ((x[i] - cx).abs() - radius).abs() > 0.001
545                    || ((y[i] - cy).abs() - radius).abs() > 0.001
546            })
547        {
548            return None;
549        }
550        Some([
551            self.points[0][0].min(self.points[2][0]),
552            self.points[0][1].min(self.points[2][1]),
553            self.points[0][0].max(self.points[2][0]),
554            self.points[0][1].max(self.points[2][1]),
555        ])
556    }
557
558    fn square_weights<'a>(
559        &self,
560        bounds: [f64; 4],
561        point: [f64; 2],
562        scratch: &'a mut WeightScratch,
563    ) -> &'a [f32] {
564        let [left, bottom, right, top] = bounds;
565        let mut x = (point[0] - left) / (right - left);
566        let mut y = (point[1] - bottom) / (top - bottom);
567        if x <= 0.0 {
568            x = 0.00001;
569        }
570        if x >= 1.0 {
571            x = 0.99999;
572        }
573        if y <= 0.0 {
574            y = 0.00001;
575        }
576        if y >= 1.0 {
577            y = 0.99999;
578        }
579        for (i, p) in self.points.iter().enumerate() {
580            let wx = if p[0] <= (left + right) * 0.5 {
581                1.0 - x
582            } else {
583                x
584            };
585            let wy = if p[1] <= (bottom + top) * 0.5 {
586                1.0 - y
587            } else {
588                y
589            };
590            scratch.weights[i] = (wx * wy) as f32;
591        }
592        &scratch.weights
593    }
594
595    pub fn new(points: Vec<[f64; 2]>) -> Result<Self, MapError> {
596        let n = points.len();
597        if n < 3 || points.iter().flatten().any(|v| !v.is_finite()) {
598            return Err(MapError::Invalid(
599                "sloping polygon requires at least three finite endpoints".into(),
600            ));
601        }
602        let corner_products = (0..n)
603            .map(|j| cross(points[j], points[(j + 1) % n], points[(j + n - 1) % n]))
604            .collect();
605        Ok(Self {
606            points,
607            corner_products,
608        })
609    }
610
611    pub fn weights<'a>(
612        &self,
613        point: [f64; 2],
614        scratch: &'a mut WeightScratch,
615    ) -> Result<&'a [f32], MapError> {
616        let n = self.points.len();
617        scratch.clipped = false;
618        if scratch.weights.len() != n || point.iter().any(|v| !v.is_finite()) {
619            return Err(MapError::Invalid(
620                "sloping weight scratch dimension or query is invalid".into(),
621            ));
622        }
623        for i in 0..n {
624            let value = cross(point, self.points[i], self.points[(i + 1) % n]) as f32;
625            scratch.cross[i] = if value == 0.0 { 0.00001_f32 } else { value };
626        }
627        let mut total = 0.0_f64;
628        for j in 0..n {
629            let previous = (j + n - 1) % n;
630            let mut product = 1.0_f64;
631            for k in 0..n {
632                if k != j && k != previous {
633                    product *= f64::from(scratch.cross[k]);
634                }
635            }
636            scratch.products[j] = product * self.corner_products[j];
637            total += scratch.products[j];
638        }
639        let mut has_negative_product = false;
640        for j in 0..n {
641            if scratch.products[j] < 0.0 {
642                has_negative_product = true;
643            }
644            scratch.weights[j] = (scratch.products[j] / total) as f32;
645            if scratch.weights[j] == 0.0 && scratch.products[j] != 0.0 {
646                scratch.clipped = true;
647            }
648        }
649        if has_negative_product {
650            let mut total = 0.0_f32;
651            for value in &mut scratch.weights {
652                if *value < 0.0 {
653                    scratch.clipped = true;
654                    *value = 0.0;
655                } else {
656                    total += *value;
657                }
658            }
659            if total != 0.0 {
660                for value in &mut scratch.weights {
661                    *value /= total;
662                }
663            }
664        }
665        Ok(&scratch.weights)
666    }
667}
668
669/// Match native binary32 offset accumulation, including its nodata convention.
670pub fn paint_corner_values(weights: &[f32], values: &[f32]) -> Result<f32, MapError> {
671    if weights.len() != values.len() || values.is_empty() {
672        return Err(MapError::Invalid(
673            "sloping paint dimensions do not match".into(),
674        ));
675    }
676    let anchor = values[0];
677    let mut delta = 0.0_f32;
678    let mut count = 0;
679    for (&weight, &value) in weights.iter().zip(values) {
680        let difference = value - anchor;
681        if difference != NODATA {
682            count += 1;
683            delta += weight * difference;
684        }
685    }
686    Ok(if count > 0 { delta + anchor } else { NODATA })
687}
688
689/// Native plot tolerance is part of the method; there is no extra depth cutoff.
690pub fn deliver_pixel(wse: f32, terrain: f32, native_tolerance: f32) -> Option<[f32; 2]> {
691    if !wse.is_finite()
692        || !terrain.is_finite()
693        || wse == NODATA
694        || terrain == NODATA
695        || wse < terrain + native_tolerance
696    {
697        None
698    } else {
699        Some([wse - terrain, wse])
700    }
701}
702
703pub struct CellStencil {
704    pub physical_ring: Vec<[f64; 2]>,
705    polygon: PolygonStencil,
706    square: Option<[f64; 4]>,
707    corners: Vec<[usize; 2]>, // vertex, incident-face slot
708    faces: Vec<[usize; 2]>,   // face, cell side
709    elevations: Option<Vec<f32>>,
710}
711
712/// Prepare immutable geometry once. Profiles never determine ring ordering.
713pub fn prepare_stencils(
714    mesh: &Mesh,
715    cell_faces: &[Vec<usize>],
716) -> Result<Vec<Option<CellStencil>>, MapError> {
717    mesh.validate()?;
718    if cell_faces.len() != mesh.cells.len() {
719        return Err(MapError::Invalid(
720            "sloping cell-face table length mismatch".into(),
721        ));
722    }
723    cell_faces
724        .iter()
725        .enumerate()
726        .map(|(cell, faces)| {
727            if mesh.cells[cell].virtual_cell {
728                return Ok(None);
729            }
730            let mut points = Vec::with_capacity(faces.len());
731            let mut corners = Vec::with_capacity(faces.len());
732            for &id in faces {
733                let face = mesh
734                    .faces
735                    .get(id)
736                    .ok_or_else(|| MapError::Invalid("cell face out of range".into()))?;
737                let vertex = if face.cells[0] == cell {
738                    face.vertices[0]
739                } else if face.cells[1] == cell {
740                    face.vertices[1]
741                } else {
742                    return Err(MapError::Invalid("cell not incident to face".into()));
743                };
744                let corner = &mesh.corners[vertex];
745                let slot =
746                    corner.faces.iter().position(|&f| f == id).ok_or_else(|| {
747                        MapError::Invalid("cell face not incident to corner".into())
748                    })?;
749                points.push(corner.point);
750                corners.push([vertex, slot]);
751            }
752            let polygon = PolygonStencil::new(points)?;
753            let square = polygon.square_bounds();
754            Ok(Some(CellStencil {
755                physical_ring: Vec::new(),
756                polygon,
757                square,
758                corners,
759                faces: faces
760                    .iter()
761                    .map(|&id| [id, usize::from(mesh.faces[id].cells[0] != cell)])
762                    .collect(),
763                elevations: None,
764            }))
765        })
766        .collect()
767}
768
769impl CellStencil {
770    /// Corner (vertex, incident-face slot) pairs in ring order.
771    pub(crate) fn corner_slots(&self) -> impl Iterator<Item = [usize; 2]> + '_ {
772        self.corners.iter().copied()
773    }
774    /// Face ids in ring order (face i joins corner i and corner i + 1).
775    pub(crate) fn face_ids(&self) -> impl Iterator<Item = usize> + '_ {
776        self.faces.iter().map(|f| f[0])
777    }
778    /// Native face-donation node weights at `point`: `n` corner weights then
779    /// `n` face weights, binary32, as used by cell-face sloping painting.
780    pub(crate) fn donated_weights(
781        &self,
782        point: [f64; 2],
783        scratch: &mut WeightScratch,
784    ) -> Result<Vec<f32>, MapError> {
785        self.donated_weights_with_history(point, scratch, false)
786    }
787
788    /// True when the last weights were clipped such that a corner's neighbours
789    /// both carry zero weight: native donation then depends on scratch history.
790    pub(crate) fn history_sensitive(&self, scratch: &WeightScratch) -> bool {
791        let n = self.corners.len();
792        scratch.clipped
793            && (0..n)
794                .any(|k| scratch.weights[(k + n - 1) % n] + scratch.weights[(k + 1) % n] == 0.0)
795    }
796
797    /// As `donated_weights`; with `retain`, clockwise/counter-clockwise
798    /// donations of corners with zero-weight neighbours keep the previous
799    /// pixel's values (native scratch history).
800    pub(crate) fn donated_weights_with_history(
801        &self,
802        point: [f64; 2],
803        scratch: &mut WeightScratch,
804        retain: bool,
805    ) -> Result<Vec<f32>, MapError> {
806        let n = self.corners.len();
807        scratch.resize(n);
808        if let Some(bounds) = self.square {
809            self.polygon.square_weights(bounds, point, scratch);
810        } else {
811            self.polygon.weights(point, scratch)?;
812        }
813        for i in 0..n {
814            let previous = (i + n - 1) % n;
815            let next = (i + 1) % n;
816            let adjacent = scratch.weights[previous] + scratch.weights[next];
817            if !retain {
818                scratch.clockwise[i] = 0.0;
819                scratch.counterclockwise[i] = 0.0;
820            }
821            if adjacent != 0.0 {
822                let fraction = scratch.weights[i] / adjacent;
823                scratch.clockwise[i] = fraction * scratch.weights[previous];
824                scratch.counterclockwise[i] = fraction * scratch.weights[next];
825            }
826            scratch.donated[i] = scratch.weights[i];
827        }
828        for i in 0..n {
829            let next = (i + 1) % n;
830            let donation = scratch.counterclockwise[i].min(scratch.clockwise[next]);
831            scratch.donated[i] -= donation;
832            scratch.donated[next] -= donation;
833            scratch.donated[n + i] = donation * 2.0;
834        }
835        Ok(scratch.donated[..2 * n].to_vec())
836    }
837}
838
839/// Optional native stored corner elevations, required only for depth weighting.
840pub fn set_vertex_elevations(
841    stencils: &mut [Option<CellStencil>],
842    elevations: &[f32],
843    vertex_count: usize,
844) -> Result<(), MapError> {
845    if elevations.len() != vertex_count || elevations.iter().any(|x| !x.is_finite()) {
846        return Err(MapError::Invalid(
847            "sloping corner elevation dimensions or values invalid".into(),
848        ));
849    }
850    for stencil in stencils.iter_mut().flatten() {
851        stencil.elevations = Some(
852            stencil
853                .corners
854                .iter()
855                .map(|&[vertex, _]| elevations[vertex])
856                .collect(),
857        );
858    }
859    Ok(())
860}
861
862pub struct PreparedProfile {
863    local_values: Vec<Vec<f32>>,
864    face_values: Vec<Vec<f32>>,
865    depth_weights: Vec<Vec<f32>>,
866    horizontal: Vec<Option<f32>>,
867    wet: Vec<bool>,
868    options: Options,
869}
870
871impl PreparedProfile {
872    /// Retained heap allocation including nested vector headers/capacities.
873    pub(crate) fn retained_bytes(&self) -> u64 {
874        fn nested(v: &Vec<Vec<f32>>) -> usize {
875            v.capacity() * std::mem::size_of::<Vec<f32>>()
876                + v.iter().map(|v| v.capacity() * 4).sum::<usize>()
877        }
878        (std::mem::size_of::<Self>()
879            + nested(&self.local_values)
880            + nested(&self.face_values)
881            + nested(&self.depth_weights)
882            + self.horizontal.capacity() * std::mem::size_of::<Option<f32>>()
883            + self.wet.capacity()) as u64
884    }
885    /// Native wet gate of a cell: WSE above its minimum plus the plot tolerance.
886    pub fn is_wet(&self, cell: usize) -> bool {
887        self.wet[cell]
888    }
889}
890
891pub fn prepare_profile(
892    mesh: &Mesh,
893    stencils: &[Option<CellStencil>],
894    values: &[f32],
895    tolerance: f32,
896    pool: &rayon::ThreadPool,
897) -> Result<PreparedProfile, MapError> {
898    prepare_profile_with_options(mesh, stencils, values, tolerance, pool, Options::default())
899}
900
901pub fn prepare_profile_with_options(
902    mesh: &Mesh,
903    stencils: &[Option<CellStencil>],
904    values: &[f32],
905    tolerance: f32,
906    pool: &rayon::ThreadPool,
907    options: Options,
908) -> Result<PreparedProfile, MapError> {
909    options.validate()?;
910    if stencils.len() != mesh.cells.len() {
911        return Err(MapError::Invalid("sloping stencil count mismatch".into()));
912    }
913    if options.use_depth_weighted_faces && stencils.iter().flatten().any(|s| s.elevations.is_none())
914    {
915        return Err(MapError::Unsupported(
916            "depth weighting requires stored native FacePoints Elevation".into(),
917        ));
918    }
919    let reconstructed = mesh.reconstruct(values, tolerance, pool)?;
920    let local_values: Vec<Vec<f32>> = pool.install(|| {
921        stencils
922            .par_iter()
923            .map(|s| match s {
924                Some(s) => s
925                    .corners
926                    .iter()
927                    .map(|&[vertex, slot]| reconstructed.corners[vertex][slot])
928                    .collect(),
929                None => Vec::new(),
930            })
931            .collect()
932    });
933    let face_values: Vec<Vec<f32>> = if options.render_mode == RenderMode::WithFaces {
934        pool.install(|| {
935            stencils
936                .par_iter()
937                .map(|s| match s {
938                    Some(s) => s
939                        .faces
940                        .iter()
941                        .map(|&[face, side]| reconstructed.faces[face].values[side])
942                        .collect(),
943                    None => Vec::new(),
944                })
945                .collect()
946        })
947    } else {
948        Vec::new()
949    };
950    let depth_weights = if options.use_depth_weighted_faces {
951        pool.install(|| {
952            stencils
953                .par_iter()
954                .enumerate()
955                .map(|(cell, s)| match s {
956                    Some(s) => {
957                        let mut result: Vec<_> = local_values[cell]
958                            .iter()
959                            .zip(s.elevations.as_ref().unwrap())
960                            .map(|(&w, &z)| depth_weight(w, z))
961                            .collect();
962                        result.extend(
963                            s.faces
964                                .iter()
965                                .zip(&face_values[cell])
966                                .map(|(&[face, _], &w)| depth_weight(w, mesh.faces[face].minimum)),
967                        );
968                        result
969                    }
970                    None => Vec::new(),
971                })
972                .collect()
973        })
974    } else {
975        Vec::new()
976    };
977    let horizontal = stencils
978        .iter()
979        .enumerate()
980        .map(|(cell, s)| {
981            s.as_ref().and_then(|s| {
982                (options.reduce_shallow_to_horizontal
983                    && s.faces.iter().all(|&[face, _]| {
984                        let c = reconstructed.faces[face].connection;
985                        !c.connected() || c == Connection::DownhillShallow
986                    }))
987                .then_some(values[cell])
988            })
989        })
990        .collect();
991    let wet = mesh
992        .cells
993        .iter()
994        .zip(values)
995        .map(|(cell, &value)| !cell.virtual_cell && value > cell.minimum + tolerance)
996        .collect();
997    Ok(PreparedProfile {
998        local_values,
999        face_values,
1000        depth_weights,
1001        horizontal,
1002        wet,
1003        options,
1004    })
1005}
1006
1007fn depth_weight(wse: f32, elevation: f32) -> f32 {
1008    if wse == NODATA || elevation == NODATA || wse <= elevation {
1009        0.01_f32
1010    } else {
1011        f64::from(wse - elevation).max(0.01_f64) as f32
1012    }
1013}
1014
1015impl WeightScratch {
1016    /// Native node donation: all arithmetic remains binary32 and in ring order.
1017    fn paint_with_faces(&mut self, corners: &[f32], faces: &[f32], depths: Option<&[f32]>) -> f32 {
1018        self.paint_with_face_history(corners, faces, depths, false)
1019    }
1020
1021    fn paint_with_face_history(
1022        &mut self,
1023        corners: &[f32],
1024        faces: &[f32],
1025        depths: Option<&[f32]>,
1026        retain: bool,
1027    ) -> f32 {
1028        let n = corners.len();
1029        for i in 0..n {
1030            let previous = (i + n - 1) % n;
1031            let next = (i + 1) % n;
1032            let adjacent = self.weights[previous] + self.weights[next];
1033            // The independent pass starts fresh; affected cells are replayed
1034            // below with native scratch retention inside each geometry batch.
1035            if !retain {
1036                self.clockwise[i] = 0.0;
1037                self.counterclockwise[i] = 0.0;
1038            }
1039            if adjacent != 0.0 {
1040                let fraction = self.weights[i] / adjacent;
1041                self.clockwise[i] = fraction * self.weights[previous];
1042                self.counterclockwise[i] = fraction * self.weights[next];
1043            }
1044            self.donated[i] = self.weights[i];
1045        }
1046        for i in 0..n {
1047            let next = (i + 1) % n;
1048            let donation = self.counterclockwise[i].min(self.clockwise[next]);
1049            self.donated[i] -= donation;
1050            self.donated[next] -= donation;
1051            self.donated[n + i] = donation * 2.0;
1052        }
1053        let mut total = 0.0_f64;
1054        if let Some(depths) = depths {
1055            for (weight, &depth) in self.donated.iter_mut().zip(depths) {
1056                *weight *= depth;
1057                total += f64::from(*weight);
1058            }
1059        }
1060        let anchor = faces[0];
1061        let mut delta = 0.0_f32;
1062        for (i, &corner) in corners.iter().enumerate() {
1063            delta += (corner - anchor) * self.donated[i];
1064        }
1065        for (i, &face) in faces.iter().enumerate() {
1066            delta += (face - anchor) * self.donated[n + i];
1067        }
1068        if depths.is_some() {
1069            (f64::from(delta) / total) as f32 + anchor
1070        } else {
1071            delta + anchor
1072        }
1073    }
1074}
1075
1076/// Internal signed-depth surface path. Raster plot tolerance is deliberately
1077/// not applied here: zero-contour interpolation needs valid dry-side samples.
1078/// The caller subtracts valid terrain from the returned water-surface value.
1079/// The resulting depth uses Option, so a numeric depth of -9999 is not missing.
1080pub(crate) fn boundary_surface_value(
1081    stencils: &[Vec<Option<CellStencil>>],
1082    profiles: &[PreparedProfile],
1083    mesh: usize,
1084    cell: usize,
1085    point: [f64; 2],
1086    scratch: &mut WeightScratch,
1087) -> Result<Option<f32>, MapError> {
1088    let profile = profiles
1089        .get(mesh)
1090        .ok_or_else(|| MapError::Invalid("boundary surface mesh out of range".into()))?;
1091    if profile.options.render_mode != RenderMode::Corners {
1092        return Err(MapError::Unsupported(
1093            "boundary contours currently require plain corner sloping; face options are not qualified".into(),
1094        ));
1095    }
1096    if point.iter().any(|v| !v.is_finite()) {
1097        return Err(MapError::Invalid("nonfinite boundary surface query".into()));
1098    }
1099    let wet = *profile
1100        .wet
1101        .get(cell)
1102        .ok_or_else(|| MapError::Invalid("boundary surface cell out of range".into()))?;
1103    if !wet {
1104        return Ok(None);
1105    }
1106    let cells = stencils
1107        .get(mesh)
1108        .ok_or_else(|| MapError::Invalid("boundary stencil mesh out of range".into()))?;
1109    let stencil = cells
1110        .get(cell)
1111        .ok_or_else(|| MapError::Invalid("boundary stencil cell out of range".into()))?;
1112    let Some(stencil) = stencil else {
1113        return Ok(None);
1114    };
1115    if let Some(value) = profile.horizontal[cell] {
1116        return Ok((value != NODATA).then_some(value));
1117    }
1118    let values = &profile.local_values[cell];
1119    scratch.resize(values.len());
1120    let weights = if let Some(bounds) = stencil.square {
1121        stencil.polygon.square_weights(bounds, point, scratch)
1122    } else {
1123        stencil.polygon.weights(point, scratch)?
1124    };
1125    let value = paint_corner_values(weights, values)?;
1126    if !value.is_finite() {
1127        return Err(MapError::Invalid(
1128            "nonfinite boundary surface interpolation".into(),
1129        ));
1130    }
1131    Ok((value != NODATA).then_some(value))
1132}
1133
1134/// Geometry and terrain belong to the caller and can be reused across timesteps.
1135/// Labels index `owner_cells` plus one; zero is outside the physical 2D footprint.
1136pub struct RasterWindow<'a> {
1137    pub source_transform: [f64; 6],
1138    pub source_tile: [usize; 2],
1139    pub width: usize,
1140    pub height: usize,
1141    pub transform: [f64; 6],
1142    pub owner: &'a [u32],
1143    pub terrain: &'a [f32],
1144    pub owner_cells: &'a [[usize; 2]], // mesh, local cell
1145}
1146
1147/// Bounded immutable geometry-only weights for a prefix of one output window.
1148/// Face rendering keeps its original scratch/history path.
1149pub(crate) struct PixelWeights {
1150    stride: usize,
1151    values: Vec<f32>,
1152}
1153
1154impl PixelWeights {
1155    pub(crate) fn prepare(
1156        stencils: &[Vec<Option<CellStencil>>],
1157        window: &RasterWindow<'_>,
1158        budget: usize,
1159        pool: &rayon::ThreadPool,
1160    ) -> Result<Self, MapError> {
1161        let stride = stencils
1162            .iter()
1163            .flatten()
1164            .filter_map(|s| s.as_ref())
1165            .map(|s| s.polygon.points.len())
1166            .max()
1167            .unwrap_or(1);
1168        if window.width == 0
1169            || window
1170                .owner
1171                .iter()
1172                .any(|&label| label as usize > window.owner_cells.len())
1173            || window
1174                .owner_cells
1175                .iter()
1176                .any(|&[mesh, cell]| mesh >= stencils.len() || cell >= stencils[mesh].len())
1177        {
1178            return Err(MapError::Invalid("invalid cached pixel addresses".into()));
1179        }
1180        let row_bytes = stride
1181            .checked_mul(4)
1182            .ok_or_else(|| MapError::Budget("weight cache stride overflow".into()))?;
1183        let count = window.owner.len().min(budget / row_bytes);
1184        let mut values = vec![0.0; count * stride];
1185        pool.install(|| {
1186            values.par_chunks_mut(stride).enumerate().try_for_each_init(
1187                || WeightScratch::new(0),
1188                |scratch, (i, row)| {
1189                    let label = window.owner[i] as usize;
1190                    if label != 0 {
1191                        let [mesh, cell] = window.owner_cells[label - 1];
1192                        if let Some(stencil) = &stencils[mesh][cell] {
1193                            scratch.resize(stencil.polygon.points.len());
1194                            let point = [
1195                                window.transform[0]
1196                                    + ((i % window.width) as f64 + 0.5) * window.transform[1],
1197                                window.transform[3]
1198                                    + ((i / window.width) as f64 + 0.5) * window.transform[5],
1199                            ];
1200                            let weights = if let Some(bounds) = stencil.square {
1201                                stencil.polygon.square_weights(bounds, point, scratch)
1202                            } else {
1203                                stencil.polygon.weights(point, scratch)?
1204                            };
1205                            row[..weights.len()].copy_from_slice(weights);
1206                        }
1207                    }
1208                    Ok::<_, MapError>(())
1209                },
1210            )
1211        })?;
1212        Ok(Self { stride, values })
1213    }
1214    pub fn bytes(&self) -> usize {
1215        self.values.len() * 4
1216    }
1217    fn get(&self, index: usize, len: usize) -> Option<&[f32]> {
1218        (index < self.values.len() / self.stride && len <= self.stride)
1219            .then(|| &self.values[index * self.stride..index * self.stride + len])
1220    }
1221}
1222
1223pub fn render_profile(
1224    stencils: &[Vec<Option<CellStencil>>],
1225    profiles: &[PreparedProfile],
1226    window: &RasterWindow<'_>,
1227    tolerance: f32,
1228    pool: &rayon::ThreadPool,
1229) -> Result<(Vec<f32>, Vec<f32>), MapError> {
1230    render_profile_cached(stencils, profiles, window, tolerance, pool, None)
1231}
1232
1233pub(crate) fn render_profile_cached(
1234    stencils: &[Vec<Option<CellStencil>>],
1235    profiles: &[PreparedProfile],
1236    window: &RasterWindow<'_>,
1237    tolerance: f32,
1238    pool: &rayon::ThreadPool,
1239    cache: Option<&PixelWeights>,
1240) -> Result<(Vec<f32>, Vec<f32>), MapError> {
1241    let count = window
1242        .width
1243        .checked_mul(window.height)
1244        .filter(|&n| n > 0)
1245        .ok_or_else(|| MapError::Invalid("empty sloping raster window".into()))?;
1246    if window.owner.len() != count
1247        || window.terrain.len() != count
1248        || stencils.len() != profiles.len()
1249        || window.transform[2] != 0.0
1250        || window.transform[4] != 0.0
1251        || window.transform.iter().any(|v| !v.is_finite())
1252        || !tolerance.is_finite()
1253        || tolerance < 0.0
1254    {
1255        return Err(MapError::Invalid(
1256            "sloping raster inputs do not match".into(),
1257        ));
1258    }
1259    for &[mesh, cell] in window.owner_cells {
1260        if mesh >= stencils.len()
1261            || cell >= stencils[mesh].len()
1262            || cell >= profiles[mesh].local_values.len()
1263        {
1264            return Err(MapError::Invalid(
1265                "sloping raster owner-cell address out of range".into(),
1266            ));
1267        }
1268    }
1269    if window
1270        .owner
1271        .iter()
1272        .any(|&i| i as usize > window.owner_cells.len())
1273    {
1274        return Err(MapError::Invalid("sloping owner label out of range".into()));
1275    }
1276    let mut depth = vec![NODATA; count];
1277    let mut wse = vec![NODATA; count];
1278    let repair: Vec<_> = window
1279        .owner_cells
1280        .iter()
1281        .map(|_| AtomicBool::new(false))
1282        .collect();
1283    pool.install(|| {
1284        depth
1285            .par_chunks_mut(4096)
1286            .zip(wse.par_chunks_mut(4096))
1287            .enumerate()
1288            .try_for_each_init(
1289                || WeightScratch::new(0),
1290                |scratch, (chunk, (d, w))| {
1291                    for j in 0..d.len() {
1292                        let i = chunk * 4096 + j;
1293                        let label = window.owner[i] as usize;
1294                        if label == 0 {
1295                            continue;
1296                        }
1297                        let [mesh, cell] = window.owner_cells[label - 1];
1298                        if !profiles[mesh].wet[cell] {
1299                            continue;
1300                        }
1301                        if let Some(value) = profiles[mesh].horizontal[cell] {
1302                            if let Some([delta, value]) =
1303                                deliver_pixel(value, window.terrain[i], tolerance)
1304                            {
1305                                d[j] = delta;
1306                                w[j] = value;
1307                            }
1308                            continue;
1309                        }
1310                        let Some(stencil) = &stencils[mesh][cell] else {
1311                            continue;
1312                        };
1313                        let values = &profiles[mesh].local_values[cell];
1314                        scratch.resize(values.len());
1315                        let col = i % window.width;
1316                        let row = i / window.width;
1317                        let point = [
1318                            window.transform[0] + (col as f64 + 0.5) * window.transform[1],
1319                            window.transform[3] + (row as f64 + 0.5) * window.transform[5],
1320                        ];
1321                        let weights = if let Some(weights) = cache
1322                            .filter(|_| profiles[mesh].options.render_mode != RenderMode::WithFaces)
1323                            .and_then(|c| c.get(i, values.len()))
1324                        {
1325                            weights
1326                        } else if let Some(bounds) = stencil.square {
1327                            stencil.polygon.square_weights(bounds, point, scratch)
1328                        } else {
1329                            stencil.polygon.weights(point, scratch)?
1330                        };
1331                        let value = if profiles[mesh].options.render_mode == RenderMode::WithFaces {
1332                            // End the borrowed weights view before mutating scratch.
1333                            let _ = weights;
1334                            if scratch.clipped
1335                                && (0..values.len()).any(|k| {
1336                                    scratch.weights[(k + values.len() - 1) % values.len()]
1337                                        + scratch.weights[(k + 1) % values.len()]
1338                                        == 0.0
1339                                })
1340                            {
1341                                repair[label - 1].store(true, Ordering::Relaxed);
1342                            }
1343                            scratch.paint_with_faces(
1344                                values,
1345                                &profiles[mesh].face_values[cell],
1346                                profiles[mesh]
1347                                    .options
1348                                    .use_depth_weighted_faces
1349                                    .then(|| profiles[mesh].depth_weights[cell].as_slice()),
1350                            )
1351                        } else {
1352                            paint_corner_values(weights, values)?
1353                        };
1354                        if let Some([delta, value]) =
1355                            deliver_pixel(value, window.terrain[i], tolerance)
1356                        {
1357                            d[j] = delta;
1358                            w[j] = value;
1359                        }
1360                    }
1361                    Ok::<_, MapError>(())
1362                },
1363            )
1364    })?;
1365    // Native geometry donation retains scratch inside fixed 256-point ranges.
1366    // Replay only cells where clipped weights can expose that state. Replaying
1367    // complete source tiles keeps bbox/polygon outputs equal to full-map crops.
1368    let repaired = pool.install(|| {
1369        window
1370            .owner_cells
1371            .par_iter()
1372            .enumerate()
1373            .filter_map(|(label, &[mesh, cell])| {
1374                repair[label]
1375                    .load(Ordering::Relaxed)
1376                    .then_some((label + 1, mesh, cell))
1377            })
1378            .map(
1379                |(label, mesh, cell)| -> Result<Vec<(usize, [f32; 2])>, MapError> {
1380                    let stencil = stencils[mesh][cell].as_ref().unwrap();
1381                    if stencil.physical_ring.is_empty() {
1382                        return Err(MapError::Unsupported(
1383                            "native face donation replay needs full cell perimeter geometry".into(),
1384                        ));
1385                    }
1386                    let [tw, th] = window.source_tile;
1387                    if tw == 0 || th == 0 || tw.checked_mul(th).is_none_or(|n| n > 16_777_216) {
1388                        return Err(MapError::Unsupported(
1389                            "unsupported native donation tile size".into(),
1390                        ));
1391                    }
1392                    let gt = window.source_transform;
1393                    let xoff = ((window.transform[0] - gt[0]) / gt[1]).round() as i64;
1394                    let yoff = ((window.transform[3] - gt[3]) / gt[5]).round() as i64;
1395                    let minx = stencil
1396                        .physical_ring
1397                        .iter()
1398                        .map(|p| p[0])
1399                        .fold(f64::INFINITY, f64::min);
1400                    let maxx = stencil
1401                        .physical_ring
1402                        .iter()
1403                        .map(|p| p[0])
1404                        .fold(f64::NEG_INFINITY, f64::max);
1405                    let miny = stencil
1406                        .physical_ring
1407                        .iter()
1408                        .map(|p| p[1])
1409                        .fold(f64::INFINITY, f64::min);
1410                    let maxy = stencil
1411                        .physical_ring
1412                        .iter()
1413                        .map(|p| p[1])
1414                        .fold(f64::NEG_INFINITY, f64::max);
1415                    let c0 = (((minx - gt[0]) / gt[1]).floor() as i64).max(xoff).max(0);
1416                    let c1 =
1417                        (((maxx - gt[0]) / gt[1]).ceil() as i64).min(xoff + window.width as i64);
1418                    let r0 = (((maxy - gt[3]) / gt[5]).floor() as i64).max(yoff).max(0);
1419                    let r1 =
1420                        (((miny - gt[3]) / gt[5]).ceil() as i64).min(yoff + window.height as i64);
1421                    let mut output = Vec::new();
1422                    if c0 >= c1 || r0 >= r1 {
1423                        return Ok(output);
1424                    }
1425                    for ty in r0 as usize / th..=(r1 as usize - 1) / th {
1426                        for tx in c0 as usize / tw..=(c1 as usize - 1) / tw {
1427                            let tile_gt = [
1428                                gt[0] + (tx * tw) as f64 * gt[1],
1429                                gt[1],
1430                                0.,
1431                                gt[3] + (ty * th) as f64 * gt[5],
1432                                0.,
1433                                gt[5],
1434                            ];
1435                            let owned = crate::ownership::burn_ring(
1436                                &stencil.physical_ring,
1437                                terrio::ops::rasterize::Grid {
1438                                    geo_transform: tile_gt,
1439                                    cols: tw,
1440                                    rows: th,
1441                                    crs: Default::default(),
1442                                },
1443                            )?;
1444                            let values = &profiles[mesh].local_values[cell];
1445                            let faces = &profiles[mesh].face_values[cell];
1446                            let depths = profiles[mesh]
1447                                .options
1448                                .use_depth_weighted_faces
1449                                .then(|| profiles[mesh].depth_weights[cell].as_slice());
1450                            let mut scratch = WeightScratch::new(values.len());
1451                            let mut native_index = 0;
1452                            for (i, &inside) in owned.iter().enumerate() {
1453                                if inside == 0 {
1454                                    continue;
1455                                }
1456                                if native_index % 256 == 0 {
1457                                    scratch.clockwise.fill(0.);
1458                                    scratch.counterclockwise.fill(0.);
1459                                }
1460                                native_index += 1;
1461                                let point = [
1462                                    tile_gt[0] + (i % tw) as f64 * gt[1] + 0.5 * gt[1],
1463                                    tile_gt[3] + (i / tw) as f64 * gt[5] + 0.5 * gt[5],
1464                                ];
1465                                stencil.polygon.weights(point, &mut scratch)?;
1466                                let value =
1467                                    scratch.paint_with_face_history(values, faces, depths, true);
1468                                let col = (tx * tw + i % tw) as i64 - xoff;
1469                                let row = (ty * th + i / tw) as i64 - yoff;
1470                                if col < 0
1471                                    || row < 0
1472                                    || col >= window.width as i64
1473                                    || row >= window.height as i64
1474                                {
1475                                    continue;
1476                                }
1477                                let index = row as usize * window.width + col as usize;
1478                                if window.owner[index] as usize != label {
1479                                    continue;
1480                                }
1481                                output.push((
1482                                    index,
1483                                    deliver_pixel(value, window.terrain[index], tolerance)
1484                                        .unwrap_or([NODATA, NODATA]),
1485                                ));
1486                            }
1487                        }
1488                    }
1489                    Ok(output)
1490                },
1491            )
1492            .collect::<Result<Vec<_>, MapError>>()
1493    })?;
1494    for cell in repaired {
1495        for (index, [d, w]) in cell {
1496            depth[index] = d;
1497            wse[index] = w;
1498        }
1499    }
1500    Ok((depth, wse))
1501}
1502
1503#[cfg(test)]
1504mod tests {
1505    use super::*;
1506
1507    #[test]
1508    fn zero_adjacent_donation_keeps_native_batch_history() {
1509        let corners = [1., 2., 3., 4., 5., 6.];
1510        let faces = [2., 3., 4., 5., 6., 7.];
1511        let mut carried = WeightScratch::new(6);
1512        carried
1513            .weights
1514            .copy_from_slice(&[0.1, 0.2, 0.1, 0.2, 0.1, 0.3]);
1515        carried.paint_with_face_history(&corners, &faces, None, true);
1516        let previous = carried.clockwise[3];
1517        assert_ne!(previous, 0.);
1518        carried.weights.copy_from_slice(&[1., 0., 0., 0., 0., 0.]);
1519        let value = carried.paint_with_face_history(&corners, &faces, None, true);
1520        assert_eq!(carried.clockwise[3].to_bits(), previous.to_bits());
1521        let mut fresh = WeightScratch::new(6);
1522        fresh.weights.copy_from_slice(&[1., 0., 0., 0., 0., 0.]);
1523        assert_ne!(
1524            value.to_bits(),
1525            fresh.paint_with_faces(&corners, &faces, None).to_bits()
1526        );
1527    }
1528
1529    #[test]
1530    fn native_davis_donated_and_depth_rebalanced_scalar_bits() {
1531        // Independent reflection of native 7.0.1 private scalar painters on
1532        // public Davis corner/face values. Supplied depth coefficients isolate
1533        // normalization; tiny negative donated weights must not be clamped.
1534        let weights = [0x3e648a40, 0x3eda93c1, 0x3e74091d, 0x3de48a40].map(f32::from_bits);
1535        let corners = [0x42452a70, 0x4244fae3, 0x4242a6f2, 0x424466fa].map(f32::from_bits);
1536        let faces = [0x4244fae3, 0x4244fae3, 0x4243c5bf, 0x42453384].map(f32::from_bits);
1537        let depths = [
1538            0x3c23d70a, 0x3fa00000, 0x418b0000, 0x40490fd0, 0x3c23d70a, 0x3fa00000, 0x418b0000,
1539            0x40490fd0,
1540        ]
1541        .map(f32::from_bits);
1542        let mut scratch = WeightScratch::new(4);
1543        scratch.weights.copy_from_slice(&weights);
1544        assert_eq!(
1545            scratch.paint_with_faces(&corners, &faces, None).to_bits(),
1546            1111809851
1547        );
1548        let expected_donated = [
1549            0xb2000000, 0x3d7a0954, 0xb2800000, 0x3c82b774, 0x3eb52e2d, 0x3ec17700, 0x3dca4878,
1550            0x3dbd704e,
1551        ];
1552        assert_eq!(
1553            scratch
1554                .donated
1555                .iter()
1556                .map(|v| v.to_bits())
1557                .collect::<Vec<_>>(),
1558            expected_donated
1559        );
1560        assert_eq!(
1561            scratch
1562                .paint_with_faces(&corners, &faces, Some(&depths))
1563                .to_bits(),
1564            1111765762
1565        );
1566    }
1567
1568    #[test]
1569    fn davis_square_pixel_preserves_native_rounding() {
1570        // Real Davis p02 cell146 and a pixel that differed with general weights.
1571        let polygon = PolygonStencil::new(vec![
1572            [6630441.54307014, 1966773.76305611],
1573            [6630441.54307014, 1966623.76305611],
1574            [6630591.54307014, 1966623.76305611],
1575            [6630591.54307014, 1966773.76305611],
1576        ])
1577        .unwrap();
1578        let mut scratch = WeightScratch::new(4);
1579        let point = [6630507.763170041, 1966711.399494069];
1580        let values = [0x423e156c, 0x423b1da6, 0x423a9ca2, 0x423dcb4e].map(f32::from_bits);
1581        let weights = polygon.square_weights(polygon.square_bounds().unwrap(), point, &mut scratch);
1582        assert_eq!(
1583            paint_corner_values(weights, &values).unwrap().to_bits(),
1584            0x423caebe
1585        );
1586        let general = polygon.weights(point, &mut scratch).unwrap();
1587        assert_ne!(
1588            paint_corner_values(general, &values).unwrap().to_bits(),
1589            0x423caebe
1590        );
1591    }
1592
1593    #[test]
1594    fn bounded_pixel_weights_preserve_square_bits_and_fall_back_outside_prefix() {
1595        let polygon = PolygonStencil::new(vec![[0., 0.], [0., 2.], [2., 2.], [2., 0.]]).unwrap();
1596        let square = polygon.square_bounds();
1597        let stencil = CellStencil {
1598            physical_ring: polygon.points.clone(),
1599            polygon,
1600            square,
1601            corners: vec![],
1602            faces: vec![],
1603            elevations: None,
1604        };
1605        let stencils = vec![vec![Some(stencil)]];
1606        let window = RasterWindow {
1607            source_transform: [0., 1., 0., 2., 0., -1.],
1608            source_tile: [2, 2],
1609            width: 2,
1610            height: 2,
1611            transform: [0., 1., 0., 2., 0., -1.],
1612            owner: &[1, 1, 1, 1],
1613            terrain: &[0.; 4],
1614            owner_cells: &[[0, 0]],
1615        };
1616        let pool = rayon::ThreadPoolBuilder::new()
1617            .num_threads(2)
1618            .build()
1619            .unwrap();
1620        let weights = PixelWeights::prepare(&stencils, &window, 32, &pool).unwrap();
1621        assert_eq!(weights.bytes(), 32);
1622        let stencil = stencils[0][0].as_ref().unwrap();
1623        let mut scratch = WeightScratch::new(4);
1624        for i in 0..2 {
1625            let point = [i as f64 + 0.5, 1.5];
1626            let expected = stencil
1627                .polygon
1628                .square_weights(square.unwrap(), point, &mut scratch);
1629            assert_eq!(
1630                weights
1631                    .get(i, 4)
1632                    .unwrap()
1633                    .iter()
1634                    .map(|v| v.to_bits())
1635                    .collect::<Vec<_>>(),
1636                expected.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
1637            );
1638        }
1639        assert!(weights.get(2, 4).is_none());
1640        let empty = PixelWeights::prepare(&stencils, &window, 0, &pool).unwrap();
1641        assert_eq!(empty.bytes(), 0);
1642        assert!(empty.get(0, 4).is_none());
1643    }
1644
1645    #[test]
1646    fn davis_endpoint_intersection_uses_polyline_midpoint() {
1647        let point = face_application(
1648            &[
1649                [6628997.072593668, 1968668.2335325822],
1650                [6629091.54307014, 1968573.76305611],
1651            ],
1652            [
1653                [6629166.54307014, 1968648.76305611],
1654                [6629016.54307014, 1968498.76305611],
1655            ],
1656        )
1657        .unwrap();
1658        assert_eq!(point, [6629044.307831904, 1968620.998294346]);
1659    }
1660
1661    #[test]
1662    fn native_plot_tolerance_is_not_an_extra_depth_cutoff() {
1663        assert!(deliver_pixel(10.0005, 10.0, 0.001).is_none());
1664        assert!(deliver_pixel(10.01, 10.0, 0.001).is_some());
1665        // Native f32 terrain+tolerance may round to terrain at large elevations.
1666        assert_eq!(
1667            deliver_pixel(100000.0, 100000.0, 0.001),
1668            Some([0.0, 100000.0])
1669        );
1670        assert!(deliver_pixel(NODATA, 10.0, 0.001).is_none());
1671    }
1672
1673    #[test]
1674    fn invalid_face_geometry_is_rejected() {
1675        assert!(face_application(&[[0.0, f64::NAN], [1.0, 1.0]], [[0.0; 2]; 2]).is_err());
1676        assert!(face_application(&[[0.0; 2]], [[0.0; 2]; 2]).is_err());
1677    }
1678}
1679
1680#[cfg(test)]
1681mod boundary_surface_tests {
1682    use super::*;
1683    fn fixture() -> (Vec<Vec<Option<CellStencil>>>, Vec<PreparedProfile>) {
1684        let points = vec![[0., 0.], [1., 0.], [1., 1.], [0., 1.]];
1685        let polygon = PolygonStencil::new(points.clone()).unwrap();
1686        let stencil = CellStencil {
1687            physical_ring: points,
1688            polygon,
1689            square: Some([0., 0., 1., 1.]),
1690            corners: vec![[0, 0]; 4],
1691            faces: vec![],
1692            elevations: None,
1693        };
1694        let profile = PreparedProfile {
1695            local_values: vec![vec![2.; 4]],
1696            face_values: vec![vec![]],
1697            depth_weights: vec![vec![]],
1698            horizontal: vec![None],
1699            wet: vec![true],
1700            options: Options::default(),
1701        };
1702        (vec![vec![Some(stencil)]], vec![profile])
1703    }
1704    #[test]
1705    fn signed_boundary_surface_preserves_dry_side_without_changing_raster_visibility() {
1706        let (stencils, profiles) = fixture();
1707        let mut scratch = WeightScratch::new(0);
1708        let surface = boundary_surface_value(&stencils, &profiles, 0, 0, [0.5, 0.5], &mut scratch)
1709            .unwrap()
1710            .unwrap();
1711        assert_eq!(surface.to_bits(), 2f32.to_bits());
1712        assert_eq!(Some(surface - 10001.), Some(-9999.));
1713        assert!(deliver_pixel(surface, 10001., 0.001).is_none());
1714    }
1715    #[test]
1716    fn boundary_surface_rejects_faces_and_retains_explicit_missing_cells() {
1717        let (stencils, mut profiles) = fixture();
1718        let mut scratch = WeightScratch::new(0);
1719        profiles[0].wet[0] = false;
1720        assert_eq!(
1721            boundary_surface_value(&stencils, &profiles, 0, 0, [0.5, 0.5], &mut scratch).unwrap(),
1722            None
1723        );
1724        profiles[0].options.render_mode = RenderMode::WithFaces;
1725        assert!(matches!(
1726            boundary_surface_value(&stencils, &profiles, 0, 0, [0.5, 0.5], &mut scratch),
1727            Err(MapError::Unsupported(_))
1728        ));
1729    }
1730}