1use crate::MapError;
8use rayon::prelude::*;
9use std::sync::atomic::{AtomicBool, Ordering};
10
11#[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
45pub 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 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
161pub 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
268fn 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 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
467pub struct PolygonStencil {
469 points: Vec<[f64; 2]>,
470 corner_products: Vec<f64>,
471}
472
473pub 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 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 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
669pub 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
689pub 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]>, faces: Vec<[usize; 2]>, elevations: Option<Vec<f32>>,
710}
711
712pub 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 pub(crate) fn corner_slots(&self) -> impl Iterator<Item = [usize; 2]> + '_ {
772 self.corners.iter().copied()
773 }
774 pub(crate) fn face_ids(&self) -> impl Iterator<Item = usize> + '_ {
776 self.faces.iter().map(|f| f[0])
777 }
778 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 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 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
839pub 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 pub fn is_wet(&self, cell: usize) -> bool {
874 self.wet[cell]
875 }
876}
877
878pub fn prepare_profile(
879 mesh: &Mesh,
880 stencils: &[Option<CellStencil>],
881 values: &[f32],
882 tolerance: f32,
883 pool: &rayon::ThreadPool,
884) -> Result<PreparedProfile, MapError> {
885 prepare_profile_with_options(mesh, stencils, values, tolerance, pool, Options::default())
886}
887
888pub fn prepare_profile_with_options(
889 mesh: &Mesh,
890 stencils: &[Option<CellStencil>],
891 values: &[f32],
892 tolerance: f32,
893 pool: &rayon::ThreadPool,
894 options: Options,
895) -> Result<PreparedProfile, MapError> {
896 options.validate()?;
897 if stencils.len() != mesh.cells.len() {
898 return Err(MapError::Invalid("sloping stencil count mismatch".into()));
899 }
900 if options.use_depth_weighted_faces && stencils.iter().flatten().any(|s| s.elevations.is_none())
901 {
902 return Err(MapError::Unsupported(
903 "depth weighting requires stored native FacePoints Elevation".into(),
904 ));
905 }
906 let reconstructed = mesh.reconstruct(values, tolerance, pool)?;
907 let local_values: Vec<Vec<f32>> = pool.install(|| {
908 stencils
909 .par_iter()
910 .map(|s| match s {
911 Some(s) => s
912 .corners
913 .iter()
914 .map(|&[vertex, slot]| reconstructed.corners[vertex][slot])
915 .collect(),
916 None => Vec::new(),
917 })
918 .collect()
919 });
920 let face_values: Vec<Vec<f32>> = if options.render_mode == RenderMode::WithFaces {
921 pool.install(|| {
922 stencils
923 .par_iter()
924 .map(|s| match s {
925 Some(s) => s
926 .faces
927 .iter()
928 .map(|&[face, side]| reconstructed.faces[face].values[side])
929 .collect(),
930 None => Vec::new(),
931 })
932 .collect()
933 })
934 } else {
935 Vec::new()
936 };
937 let depth_weights = if options.use_depth_weighted_faces {
938 pool.install(|| {
939 stencils
940 .par_iter()
941 .enumerate()
942 .map(|(cell, s)| match s {
943 Some(s) => {
944 let mut result: Vec<_> = local_values[cell]
945 .iter()
946 .zip(s.elevations.as_ref().unwrap())
947 .map(|(&w, &z)| depth_weight(w, z))
948 .collect();
949 result.extend(
950 s.faces
951 .iter()
952 .zip(&face_values[cell])
953 .map(|(&[face, _], &w)| depth_weight(w, mesh.faces[face].minimum)),
954 );
955 result
956 }
957 None => Vec::new(),
958 })
959 .collect()
960 })
961 } else {
962 Vec::new()
963 };
964 let horizontal = stencils
965 .iter()
966 .enumerate()
967 .map(|(cell, s)| {
968 s.as_ref().and_then(|s| {
969 (options.reduce_shallow_to_horizontal
970 && s.faces.iter().all(|&[face, _]| {
971 let c = reconstructed.faces[face].connection;
972 !c.connected() || c == Connection::DownhillShallow
973 }))
974 .then_some(values[cell])
975 })
976 })
977 .collect();
978 let wet = mesh
979 .cells
980 .iter()
981 .zip(values)
982 .map(|(cell, &value)| !cell.virtual_cell && value > cell.minimum + tolerance)
983 .collect();
984 Ok(PreparedProfile {
985 local_values,
986 face_values,
987 depth_weights,
988 horizontal,
989 wet,
990 options,
991 })
992}
993
994fn depth_weight(wse: f32, elevation: f32) -> f32 {
995 if wse == NODATA || elevation == NODATA || wse <= elevation {
996 0.01_f32
997 } else {
998 f64::from(wse - elevation).max(0.01_f64) as f32
999 }
1000}
1001
1002impl WeightScratch {
1003 fn paint_with_faces(&mut self, corners: &[f32], faces: &[f32], depths: Option<&[f32]>) -> f32 {
1005 self.paint_with_face_history(corners, faces, depths, false)
1006 }
1007
1008 fn paint_with_face_history(
1009 &mut self,
1010 corners: &[f32],
1011 faces: &[f32],
1012 depths: Option<&[f32]>,
1013 retain: bool,
1014 ) -> f32 {
1015 let n = corners.len();
1016 for i in 0..n {
1017 let previous = (i + n - 1) % n;
1018 let next = (i + 1) % n;
1019 let adjacent = self.weights[previous] + self.weights[next];
1020 if !retain {
1023 self.clockwise[i] = 0.0;
1024 self.counterclockwise[i] = 0.0;
1025 }
1026 if adjacent != 0.0 {
1027 let fraction = self.weights[i] / adjacent;
1028 self.clockwise[i] = fraction * self.weights[previous];
1029 self.counterclockwise[i] = fraction * self.weights[next];
1030 }
1031 self.donated[i] = self.weights[i];
1032 }
1033 for i in 0..n {
1034 let next = (i + 1) % n;
1035 let donation = self.counterclockwise[i].min(self.clockwise[next]);
1036 self.donated[i] -= donation;
1037 self.donated[next] -= donation;
1038 self.donated[n + i] = donation * 2.0;
1039 }
1040 let mut total = 0.0_f64;
1041 if let Some(depths) = depths {
1042 for (weight, &depth) in self.donated.iter_mut().zip(depths) {
1043 *weight *= depth;
1044 total += f64::from(*weight);
1045 }
1046 }
1047 let anchor = faces[0];
1048 let mut delta = 0.0_f32;
1049 for (i, &corner) in corners.iter().enumerate() {
1050 delta += (corner - anchor) * self.donated[i];
1051 }
1052 for (i, &face) in faces.iter().enumerate() {
1053 delta += (face - anchor) * self.donated[n + i];
1054 }
1055 if depths.is_some() {
1056 (f64::from(delta) / total) as f32 + anchor
1057 } else {
1058 delta + anchor
1059 }
1060 }
1061}
1062
1063pub(crate) fn boundary_surface_value(
1068 stencils: &[Vec<Option<CellStencil>>],
1069 profiles: &[PreparedProfile],
1070 mesh: usize,
1071 cell: usize,
1072 point: [f64; 2],
1073 scratch: &mut WeightScratch,
1074) -> Result<Option<f32>, MapError> {
1075 let profile = profiles
1076 .get(mesh)
1077 .ok_or_else(|| MapError::Invalid("boundary surface mesh out of range".into()))?;
1078 if profile.options.render_mode != RenderMode::Corners {
1079 return Err(MapError::Unsupported(
1080 "boundary contours currently require plain corner sloping; face options are not qualified".into(),
1081 ));
1082 }
1083 if point.iter().any(|v| !v.is_finite()) {
1084 return Err(MapError::Invalid("nonfinite boundary surface query".into()));
1085 }
1086 let wet = *profile
1087 .wet
1088 .get(cell)
1089 .ok_or_else(|| MapError::Invalid("boundary surface cell out of range".into()))?;
1090 if !wet {
1091 return Ok(None);
1092 }
1093 let cells = stencils
1094 .get(mesh)
1095 .ok_or_else(|| MapError::Invalid("boundary stencil mesh out of range".into()))?;
1096 let stencil = cells
1097 .get(cell)
1098 .ok_or_else(|| MapError::Invalid("boundary stencil cell out of range".into()))?;
1099 let Some(stencil) = stencil else {
1100 return Ok(None);
1101 };
1102 if let Some(value) = profile.horizontal[cell] {
1103 return Ok((value != NODATA).then_some(value));
1104 }
1105 let values = &profile.local_values[cell];
1106 scratch.resize(values.len());
1107 let weights = if let Some(bounds) = stencil.square {
1108 stencil.polygon.square_weights(bounds, point, scratch)
1109 } else {
1110 stencil.polygon.weights(point, scratch)?
1111 };
1112 let value = paint_corner_values(weights, values)?;
1113 if !value.is_finite() {
1114 return Err(MapError::Invalid(
1115 "nonfinite boundary surface interpolation".into(),
1116 ));
1117 }
1118 Ok((value != NODATA).then_some(value))
1119}
1120
1121pub struct RasterWindow<'a> {
1124 pub source_transform: [f64; 6],
1125 pub source_tile: [usize; 2],
1126 pub width: usize,
1127 pub height: usize,
1128 pub transform: [f64; 6],
1129 pub owner: &'a [u32],
1130 pub terrain: &'a [f32],
1131 pub owner_cells: &'a [[usize; 2]], }
1133
1134pub(crate) struct PixelWeights {
1137 stride: usize,
1138 values: Vec<f32>,
1139}
1140
1141impl PixelWeights {
1142 pub(crate) fn prepare(
1143 stencils: &[Vec<Option<CellStencil>>],
1144 window: &RasterWindow<'_>,
1145 budget: usize,
1146 pool: &rayon::ThreadPool,
1147 ) -> Result<Self, MapError> {
1148 let stride = stencils
1149 .iter()
1150 .flatten()
1151 .filter_map(|s| s.as_ref())
1152 .map(|s| s.polygon.points.len())
1153 .max()
1154 .unwrap_or(1);
1155 if window.width == 0
1156 || window
1157 .owner
1158 .iter()
1159 .any(|&label| label as usize > window.owner_cells.len())
1160 || window
1161 .owner_cells
1162 .iter()
1163 .any(|&[mesh, cell]| mesh >= stencils.len() || cell >= stencils[mesh].len())
1164 {
1165 return Err(MapError::Invalid("invalid cached pixel addresses".into()));
1166 }
1167 let row_bytes = stride
1168 .checked_mul(4)
1169 .ok_or_else(|| MapError::Budget("weight cache stride overflow".into()))?;
1170 let count = window.owner.len().min(budget / row_bytes);
1171 let mut values = vec![0.0; count * stride];
1172 pool.install(|| {
1173 values.par_chunks_mut(stride).enumerate().try_for_each_init(
1174 || WeightScratch::new(0),
1175 |scratch, (i, row)| {
1176 let label = window.owner[i] as usize;
1177 if label != 0 {
1178 let [mesh, cell] = window.owner_cells[label - 1];
1179 if let Some(stencil) = &stencils[mesh][cell] {
1180 scratch.resize(stencil.polygon.points.len());
1181 let point = [
1182 window.transform[0]
1183 + ((i % window.width) as f64 + 0.5) * window.transform[1],
1184 window.transform[3]
1185 + ((i / window.width) as f64 + 0.5) * window.transform[5],
1186 ];
1187 let weights = if let Some(bounds) = stencil.square {
1188 stencil.polygon.square_weights(bounds, point, scratch)
1189 } else {
1190 stencil.polygon.weights(point, scratch)?
1191 };
1192 row[..weights.len()].copy_from_slice(weights);
1193 }
1194 }
1195 Ok::<_, MapError>(())
1196 },
1197 )
1198 })?;
1199 Ok(Self { stride, values })
1200 }
1201 pub fn bytes(&self) -> usize {
1202 self.values.len() * 4
1203 }
1204 fn get(&self, index: usize, len: usize) -> Option<&[f32]> {
1205 (index < self.values.len() / self.stride && len <= self.stride)
1206 .then(|| &self.values[index * self.stride..index * self.stride + len])
1207 }
1208}
1209
1210pub fn render_profile(
1211 stencils: &[Vec<Option<CellStencil>>],
1212 profiles: &[PreparedProfile],
1213 window: &RasterWindow<'_>,
1214 tolerance: f32,
1215 pool: &rayon::ThreadPool,
1216) -> Result<(Vec<f32>, Vec<f32>), MapError> {
1217 render_profile_cached(stencils, profiles, window, tolerance, pool, None)
1218}
1219
1220pub(crate) fn render_profile_cached(
1221 stencils: &[Vec<Option<CellStencil>>],
1222 profiles: &[PreparedProfile],
1223 window: &RasterWindow<'_>,
1224 tolerance: f32,
1225 pool: &rayon::ThreadPool,
1226 cache: Option<&PixelWeights>,
1227) -> Result<(Vec<f32>, Vec<f32>), MapError> {
1228 let count = window
1229 .width
1230 .checked_mul(window.height)
1231 .filter(|&n| n > 0)
1232 .ok_or_else(|| MapError::Invalid("empty sloping raster window".into()))?;
1233 if window.owner.len() != count
1234 || window.terrain.len() != count
1235 || stencils.len() != profiles.len()
1236 || window.transform[2] != 0.0
1237 || window.transform[4] != 0.0
1238 || window.transform.iter().any(|v| !v.is_finite())
1239 || !tolerance.is_finite()
1240 || tolerance < 0.0
1241 {
1242 return Err(MapError::Invalid(
1243 "sloping raster inputs do not match".into(),
1244 ));
1245 }
1246 for &[mesh, cell] in window.owner_cells {
1247 if mesh >= stencils.len()
1248 || cell >= stencils[mesh].len()
1249 || cell >= profiles[mesh].local_values.len()
1250 {
1251 return Err(MapError::Invalid(
1252 "sloping raster owner-cell address out of range".into(),
1253 ));
1254 }
1255 }
1256 if window
1257 .owner
1258 .iter()
1259 .any(|&i| i as usize > window.owner_cells.len())
1260 {
1261 return Err(MapError::Invalid("sloping owner label out of range".into()));
1262 }
1263 let mut depth = vec![NODATA; count];
1264 let mut wse = vec![NODATA; count];
1265 let repair: Vec<_> = window
1266 .owner_cells
1267 .iter()
1268 .map(|_| AtomicBool::new(false))
1269 .collect();
1270 pool.install(|| {
1271 depth
1272 .par_chunks_mut(4096)
1273 .zip(wse.par_chunks_mut(4096))
1274 .enumerate()
1275 .try_for_each_init(
1276 || WeightScratch::new(0),
1277 |scratch, (chunk, (d, w))| {
1278 for j in 0..d.len() {
1279 let i = chunk * 4096 + j;
1280 let label = window.owner[i] as usize;
1281 if label == 0 {
1282 continue;
1283 }
1284 let [mesh, cell] = window.owner_cells[label - 1];
1285 if !profiles[mesh].wet[cell] {
1286 continue;
1287 }
1288 if let Some(value) = profiles[mesh].horizontal[cell] {
1289 if let Some([delta, value]) =
1290 deliver_pixel(value, window.terrain[i], tolerance)
1291 {
1292 d[j] = delta;
1293 w[j] = value;
1294 }
1295 continue;
1296 }
1297 let Some(stencil) = &stencils[mesh][cell] else {
1298 continue;
1299 };
1300 let values = &profiles[mesh].local_values[cell];
1301 scratch.resize(values.len());
1302 let col = i % window.width;
1303 let row = i / window.width;
1304 let point = [
1305 window.transform[0] + (col as f64 + 0.5) * window.transform[1],
1306 window.transform[3] + (row as f64 + 0.5) * window.transform[5],
1307 ];
1308 let weights = if let Some(weights) = cache
1309 .filter(|_| profiles[mesh].options.render_mode != RenderMode::WithFaces)
1310 .and_then(|c| c.get(i, values.len()))
1311 {
1312 weights
1313 } else if let Some(bounds) = stencil.square {
1314 stencil.polygon.square_weights(bounds, point, scratch)
1315 } else {
1316 stencil.polygon.weights(point, scratch)?
1317 };
1318 let value = if profiles[mesh].options.render_mode == RenderMode::WithFaces {
1319 let _ = weights;
1321 if scratch.clipped
1322 && (0..values.len()).any(|k| {
1323 scratch.weights[(k + values.len() - 1) % values.len()]
1324 + scratch.weights[(k + 1) % values.len()]
1325 == 0.0
1326 })
1327 {
1328 repair[label - 1].store(true, Ordering::Relaxed);
1329 }
1330 scratch.paint_with_faces(
1331 values,
1332 &profiles[mesh].face_values[cell],
1333 profiles[mesh]
1334 .options
1335 .use_depth_weighted_faces
1336 .then(|| profiles[mesh].depth_weights[cell].as_slice()),
1337 )
1338 } else {
1339 paint_corner_values(weights, values)?
1340 };
1341 if let Some([delta, value]) =
1342 deliver_pixel(value, window.terrain[i], tolerance)
1343 {
1344 d[j] = delta;
1345 w[j] = value;
1346 }
1347 }
1348 Ok::<_, MapError>(())
1349 },
1350 )
1351 })?;
1352 let repaired = pool.install(|| {
1356 window
1357 .owner_cells
1358 .par_iter()
1359 .enumerate()
1360 .filter_map(|(label, &[mesh, cell])| {
1361 repair[label]
1362 .load(Ordering::Relaxed)
1363 .then_some((label + 1, mesh, cell))
1364 })
1365 .map(
1366 |(label, mesh, cell)| -> Result<Vec<(usize, [f32; 2])>, MapError> {
1367 let stencil = stencils[mesh][cell].as_ref().unwrap();
1368 if stencil.physical_ring.is_empty() {
1369 return Err(MapError::Unsupported(
1370 "native face donation replay needs full cell perimeter geometry".into(),
1371 ));
1372 }
1373 let [tw, th] = window.source_tile;
1374 if tw == 0 || th == 0 || tw.checked_mul(th).is_none_or(|n| n > 16_777_216) {
1375 return Err(MapError::Unsupported(
1376 "unsupported native donation tile size".into(),
1377 ));
1378 }
1379 let gt = window.source_transform;
1380 let xoff = ((window.transform[0] - gt[0]) / gt[1]).round() as i64;
1381 let yoff = ((window.transform[3] - gt[3]) / gt[5]).round() as i64;
1382 let minx = stencil
1383 .physical_ring
1384 .iter()
1385 .map(|p| p[0])
1386 .fold(f64::INFINITY, f64::min);
1387 let maxx = stencil
1388 .physical_ring
1389 .iter()
1390 .map(|p| p[0])
1391 .fold(f64::NEG_INFINITY, f64::max);
1392 let miny = stencil
1393 .physical_ring
1394 .iter()
1395 .map(|p| p[1])
1396 .fold(f64::INFINITY, f64::min);
1397 let maxy = stencil
1398 .physical_ring
1399 .iter()
1400 .map(|p| p[1])
1401 .fold(f64::NEG_INFINITY, f64::max);
1402 let c0 = (((minx - gt[0]) / gt[1]).floor() as i64).max(xoff).max(0);
1403 let c1 =
1404 (((maxx - gt[0]) / gt[1]).ceil() as i64).min(xoff + window.width as i64);
1405 let r0 = (((maxy - gt[3]) / gt[5]).floor() as i64).max(yoff).max(0);
1406 let r1 =
1407 (((miny - gt[3]) / gt[5]).ceil() as i64).min(yoff + window.height as i64);
1408 let mut output = Vec::new();
1409 if c0 >= c1 || r0 >= r1 {
1410 return Ok(output);
1411 }
1412 for ty in r0 as usize / th..=(r1 as usize - 1) / th {
1413 for tx in c0 as usize / tw..=(c1 as usize - 1) / tw {
1414 let tile_gt = [
1415 gt[0] + (tx * tw) as f64 * gt[1],
1416 gt[1],
1417 0.,
1418 gt[3] + (ty * th) as f64 * gt[5],
1419 0.,
1420 gt[5],
1421 ];
1422 let owned = crate::ownership::burn_ring(
1423 &stencil.physical_ring,
1424 terrio::ops::rasterize::Grid {
1425 geo_transform: tile_gt,
1426 cols: tw,
1427 rows: th,
1428 crs: Default::default(),
1429 },
1430 )?;
1431 let values = &profiles[mesh].local_values[cell];
1432 let faces = &profiles[mesh].face_values[cell];
1433 let depths = profiles[mesh]
1434 .options
1435 .use_depth_weighted_faces
1436 .then(|| profiles[mesh].depth_weights[cell].as_slice());
1437 let mut scratch = WeightScratch::new(values.len());
1438 let mut native_index = 0;
1439 for (i, &inside) in owned.iter().enumerate() {
1440 if inside == 0 {
1441 continue;
1442 }
1443 if native_index % 256 == 0 {
1444 scratch.clockwise.fill(0.);
1445 scratch.counterclockwise.fill(0.);
1446 }
1447 native_index += 1;
1448 let point = [
1449 tile_gt[0] + (i % tw) as f64 * gt[1] + 0.5 * gt[1],
1450 tile_gt[3] + (i / tw) as f64 * gt[5] + 0.5 * gt[5],
1451 ];
1452 stencil.polygon.weights(point, &mut scratch)?;
1453 let value =
1454 scratch.paint_with_face_history(values, faces, depths, true);
1455 let col = (tx * tw + i % tw) as i64 - xoff;
1456 let row = (ty * th + i / tw) as i64 - yoff;
1457 if col < 0
1458 || row < 0
1459 || col >= window.width as i64
1460 || row >= window.height as i64
1461 {
1462 continue;
1463 }
1464 let index = row as usize * window.width + col as usize;
1465 if window.owner[index] as usize != label {
1466 continue;
1467 }
1468 output.push((
1469 index,
1470 deliver_pixel(value, window.terrain[index], tolerance)
1471 .unwrap_or([NODATA, NODATA]),
1472 ));
1473 }
1474 }
1475 }
1476 Ok(output)
1477 },
1478 )
1479 .collect::<Result<Vec<_>, MapError>>()
1480 })?;
1481 for cell in repaired {
1482 for (index, [d, w]) in cell {
1483 depth[index] = d;
1484 wse[index] = w;
1485 }
1486 }
1487 Ok((depth, wse))
1488}
1489
1490#[cfg(test)]
1491mod tests {
1492 use super::*;
1493
1494 #[test]
1495 fn zero_adjacent_donation_keeps_native_batch_history() {
1496 let corners = [1., 2., 3., 4., 5., 6.];
1497 let faces = [2., 3., 4., 5., 6., 7.];
1498 let mut carried = WeightScratch::new(6);
1499 carried
1500 .weights
1501 .copy_from_slice(&[0.1, 0.2, 0.1, 0.2, 0.1, 0.3]);
1502 carried.paint_with_face_history(&corners, &faces, None, true);
1503 let previous = carried.clockwise[3];
1504 assert_ne!(previous, 0.);
1505 carried.weights.copy_from_slice(&[1., 0., 0., 0., 0., 0.]);
1506 let value = carried.paint_with_face_history(&corners, &faces, None, true);
1507 assert_eq!(carried.clockwise[3].to_bits(), previous.to_bits());
1508 let mut fresh = WeightScratch::new(6);
1509 fresh.weights.copy_from_slice(&[1., 0., 0., 0., 0., 0.]);
1510 assert_ne!(
1511 value.to_bits(),
1512 fresh.paint_with_faces(&corners, &faces, None).to_bits()
1513 );
1514 }
1515
1516 #[test]
1517 fn native_davis_donated_and_depth_rebalanced_scalar_bits() {
1518 let weights = [0x3e648a40, 0x3eda93c1, 0x3e74091d, 0x3de48a40].map(f32::from_bits);
1522 let corners = [0x42452a70, 0x4244fae3, 0x4242a6f2, 0x424466fa].map(f32::from_bits);
1523 let faces = [0x4244fae3, 0x4244fae3, 0x4243c5bf, 0x42453384].map(f32::from_bits);
1524 let depths = [
1525 0x3c23d70a, 0x3fa00000, 0x418b0000, 0x40490fd0, 0x3c23d70a, 0x3fa00000, 0x418b0000,
1526 0x40490fd0,
1527 ]
1528 .map(f32::from_bits);
1529 let mut scratch = WeightScratch::new(4);
1530 scratch.weights.copy_from_slice(&weights);
1531 assert_eq!(
1532 scratch.paint_with_faces(&corners, &faces, None).to_bits(),
1533 1111809851
1534 );
1535 let expected_donated = [
1536 0xb2000000, 0x3d7a0954, 0xb2800000, 0x3c82b774, 0x3eb52e2d, 0x3ec17700, 0x3dca4878,
1537 0x3dbd704e,
1538 ];
1539 assert_eq!(
1540 scratch
1541 .donated
1542 .iter()
1543 .map(|v| v.to_bits())
1544 .collect::<Vec<_>>(),
1545 expected_donated
1546 );
1547 assert_eq!(
1548 scratch
1549 .paint_with_faces(&corners, &faces, Some(&depths))
1550 .to_bits(),
1551 1111765762
1552 );
1553 }
1554
1555 #[test]
1556 fn davis_square_pixel_preserves_native_rounding() {
1557 let polygon = PolygonStencil::new(vec![
1559 [6630441.54307014, 1966773.76305611],
1560 [6630441.54307014, 1966623.76305611],
1561 [6630591.54307014, 1966623.76305611],
1562 [6630591.54307014, 1966773.76305611],
1563 ])
1564 .unwrap();
1565 let mut scratch = WeightScratch::new(4);
1566 let point = [6630507.763170041, 1966711.399494069];
1567 let values = [0x423e156c, 0x423b1da6, 0x423a9ca2, 0x423dcb4e].map(f32::from_bits);
1568 let weights = polygon.square_weights(polygon.square_bounds().unwrap(), point, &mut scratch);
1569 assert_eq!(
1570 paint_corner_values(weights, &values).unwrap().to_bits(),
1571 0x423caebe
1572 );
1573 let general = polygon.weights(point, &mut scratch).unwrap();
1574 assert_ne!(
1575 paint_corner_values(general, &values).unwrap().to_bits(),
1576 0x423caebe
1577 );
1578 }
1579
1580 #[test]
1581 fn bounded_pixel_weights_preserve_square_bits_and_fall_back_outside_prefix() {
1582 let polygon = PolygonStencil::new(vec![[0., 0.], [0., 2.], [2., 2.], [2., 0.]]).unwrap();
1583 let square = polygon.square_bounds();
1584 let stencil = CellStencil {
1585 physical_ring: polygon.points.clone(),
1586 polygon,
1587 square,
1588 corners: vec![],
1589 faces: vec![],
1590 elevations: None,
1591 };
1592 let stencils = vec![vec![Some(stencil)]];
1593 let window = RasterWindow {
1594 source_transform: [0., 1., 0., 2., 0., -1.],
1595 source_tile: [2, 2],
1596 width: 2,
1597 height: 2,
1598 transform: [0., 1., 0., 2., 0., -1.],
1599 owner: &[1, 1, 1, 1],
1600 terrain: &[0.; 4],
1601 owner_cells: &[[0, 0]],
1602 };
1603 let pool = rayon::ThreadPoolBuilder::new()
1604 .num_threads(2)
1605 .build()
1606 .unwrap();
1607 let weights = PixelWeights::prepare(&stencils, &window, 32, &pool).unwrap();
1608 assert_eq!(weights.bytes(), 32);
1609 let stencil = stencils[0][0].as_ref().unwrap();
1610 let mut scratch = WeightScratch::new(4);
1611 for i in 0..2 {
1612 let point = [i as f64 + 0.5, 1.5];
1613 let expected = stencil
1614 .polygon
1615 .square_weights(square.unwrap(), point, &mut scratch);
1616 assert_eq!(
1617 weights
1618 .get(i, 4)
1619 .unwrap()
1620 .iter()
1621 .map(|v| v.to_bits())
1622 .collect::<Vec<_>>(),
1623 expected.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
1624 );
1625 }
1626 assert!(weights.get(2, 4).is_none());
1627 let empty = PixelWeights::prepare(&stencils, &window, 0, &pool).unwrap();
1628 assert_eq!(empty.bytes(), 0);
1629 assert!(empty.get(0, 4).is_none());
1630 }
1631
1632 #[test]
1633 fn davis_endpoint_intersection_uses_polyline_midpoint() {
1634 let point = face_application(
1635 &[
1636 [6628997.072593668, 1968668.2335325822],
1637 [6629091.54307014, 1968573.76305611],
1638 ],
1639 [
1640 [6629166.54307014, 1968648.76305611],
1641 [6629016.54307014, 1968498.76305611],
1642 ],
1643 )
1644 .unwrap();
1645 assert_eq!(point, [6629044.307831904, 1968620.998294346]);
1646 }
1647
1648 #[test]
1649 fn native_plot_tolerance_is_not_an_extra_depth_cutoff() {
1650 assert!(deliver_pixel(10.0005, 10.0, 0.001).is_none());
1651 assert!(deliver_pixel(10.01, 10.0, 0.001).is_some());
1652 assert_eq!(
1654 deliver_pixel(100000.0, 100000.0, 0.001),
1655 Some([0.0, 100000.0])
1656 );
1657 assert!(deliver_pixel(NODATA, 10.0, 0.001).is_none());
1658 }
1659
1660 #[test]
1661 fn invalid_face_geometry_is_rejected() {
1662 assert!(face_application(&[[0.0, f64::NAN], [1.0, 1.0]], [[0.0; 2]; 2]).is_err());
1663 assert!(face_application(&[[0.0; 2]], [[0.0; 2]; 2]).is_err());
1664 }
1665}
1666
1667#[cfg(test)]
1668mod boundary_surface_tests {
1669 use super::*;
1670 fn fixture() -> (Vec<Vec<Option<CellStencil>>>, Vec<PreparedProfile>) {
1671 let points = vec![[0., 0.], [1., 0.], [1., 1.], [0., 1.]];
1672 let polygon = PolygonStencil::new(points.clone()).unwrap();
1673 let stencil = CellStencil {
1674 physical_ring: points,
1675 polygon,
1676 square: Some([0., 0., 1., 1.]),
1677 corners: vec![[0, 0]; 4],
1678 faces: vec![],
1679 elevations: None,
1680 };
1681 let profile = PreparedProfile {
1682 local_values: vec![vec![2.; 4]],
1683 face_values: vec![vec![]],
1684 depth_weights: vec![vec![]],
1685 horizontal: vec![None],
1686 wet: vec![true],
1687 options: Options::default(),
1688 };
1689 (vec![vec![Some(stencil)]], vec![profile])
1690 }
1691 #[test]
1692 fn signed_boundary_surface_preserves_dry_side_without_changing_raster_visibility() {
1693 let (stencils, profiles) = fixture();
1694 let mut scratch = WeightScratch::new(0);
1695 let surface = boundary_surface_value(&stencils, &profiles, 0, 0, [0.5, 0.5], &mut scratch)
1696 .unwrap()
1697 .unwrap();
1698 assert_eq!(surface.to_bits(), 2f32.to_bits());
1699 assert_eq!(Some(surface - 10001.), Some(-9999.));
1700 assert!(deliver_pixel(surface, 10001., 0.001).is_none());
1701 }
1702 #[test]
1703 fn boundary_surface_rejects_faces_and_retains_explicit_missing_cells() {
1704 let (stencils, mut profiles) = fixture();
1705 let mut scratch = WeightScratch::new(0);
1706 profiles[0].wet[0] = false;
1707 assert_eq!(
1708 boundary_surface_value(&stencils, &profiles, 0, 0, [0.5, 0.5], &mut scratch).unwrap(),
1709 None
1710 );
1711 profiles[0].options.render_mode = RenderMode::WithFaces;
1712 assert!(matches!(
1713 boundary_surface_value(&stencils, &profiles, 0, 0, [0.5, 0.5], &mut scratch),
1714 Err(MapError::Unsupported(_))
1715 ));
1716 }
1717}