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(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 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 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 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
1076pub(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
1134pub 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]], }
1146
1147pub(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 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 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 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 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 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}