style(simulation): rustfmt + clippy fixes for Layer-1/heightmap code (#953 #963)

Pre-push surfaced fmt + clippy (-D warnings) failures in the new code:
- rustfmt the 6 new/changed atlas files + the bench example.
- features.rs: HashMap → BTreeMap (project bans HashMap for determinism via
  clippy disallowed_types; the bucket map is lookup-only either way).
- attractor_matching.rs: drop now-redundant .clone() on AttractorType (it
  became Copy in #953) — clippy clone_on_copy.
- drainage.rs tests: manual range → (1..=12).contains(&n).
- features.rs test: drop .clone() on Copy AttractorType.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 10:11:43 +02:00
co-authored by Claude Opus 4.7
parent 7440e933d6
commit f3e017a12f
7 changed files with 96 additions and 43 deletions
+45 -14
View File
@@ -16,7 +16,7 @@
//! No `HashMap`/`HashSet` iteration. `strength` is f32 but is never used as a
//! sort key.
use std::collections::{HashMap, VecDeque};
use std::collections::{BTreeMap, VecDeque};
use crate::atlas::drainage::DrainageResult;
use crate::atlas::heightmap::BodyHeightmap;
@@ -169,7 +169,9 @@ fn compute_lake_mask(ocean_mask: &[bool], w: usize, h: usize) -> Vec<bool> {
}
// Lakes = submerged cells in any non-ocean component.
(0..n).map(|i| comp[i] >= 0 && comp[i] != ocean_comp).collect()
(0..n)
.map(|i| comp[i] >= 0 && comp[i] != ocean_comp)
.collect()
}
/// Multi-source BFS Chebyshev distance to nearest ocean cell or river mouth.
@@ -317,14 +319,28 @@ pub fn extract_attractors(
for &(r, c) in &drainage.river_network.mouths {
let i = idx(r as usize, c as usize, w);
let s = accum[i] as f32 / max_accum;
claim(&mut out, &mut claimed, r as usize, c as usize, AttractorType::RiverMouth, s);
claim(
&mut out,
&mut claimed,
r as usize,
c as usize,
AttractorType::RiverMouth,
s,
);
}
// 2. RiverCrossing — confluences, strength = accum / max_accum * 0.7.
for &(r, c) in &drainage.river_network.confluences {
let i = idx(r as usize, c as usize, w);
let s = accum[i] as f32 / max_accum * 0.7;
claim(&mut out, &mut claimed, r as usize, c as usize, AttractorType::RiverCrossing, s);
claim(
&mut out,
&mut claimed,
r as usize,
c as usize,
AttractorType::RiverCrossing,
s,
);
}
// 3. CoastalAccess — land within 3 cells of ocean, thinned by spacing.
@@ -346,7 +362,14 @@ pub fn extract_attractors(
}
}
for (r, c, s) in thin_by_spacing(coastal, &claimed, w) {
claim(&mut out, &mut claimed, r, c, AttractorType::CoastalAccess, s);
claim(
&mut out,
&mut claimed,
r,
c,
AttractorType::CoastalAccess,
s,
);
}
// 4. ValleyFloor — gentle slope, mid elevation, positive habitability.
@@ -524,7 +547,11 @@ fn is_saddle(elev: &[f32], r: usize, c: usize, w: usize, h: usize) -> bool {
return false; // poles can't be saddles in this scheme
}
let nc = wrap_col(c as i32 + dc, w as i32);
signs[k] = if elev[idx(nr as usize, nc, w)] > e { 1 } else { -1 };
signs[k] = if elev[idx(nr as usize, nc, w)] > e {
1
} else {
-1
};
}
let mut transitions = 0;
for k in 0..8 {
@@ -540,9 +567,9 @@ fn is_saddle(elev: &[f32], r: usize, c: usize, w: usize, h: usize) -> bool {
///
/// Uses a bucket grid (cell size = `MIN_SPACING`) so each candidate only checks
/// the 3×3 neighboring buckets — O(k) amortized rather than O(k²). The kept
/// order is fully determined by the sorted candidate iteration, so the internal
/// `HashMap` (lookup only, never iterated for output) does not affect
/// determinism.
/// order is fully determined by the sorted candidate iteration; the bucket map
/// is `BTreeMap` (the project bans `HashMap` for determinism) and is lookup-only
/// regardless.
fn thin_by_spacing(
mut cands: Vec<(usize, usize, f32)>,
_claimed: &[bool],
@@ -555,7 +582,7 @@ fn thin_by_spacing(
sb.cmp(&sa).then(a.0.cmp(&b.0)).then(a.1.cmp(&b.1))
});
let sp = MIN_SPACING.max(1) as usize;
let mut buckets: HashMap<(usize, usize), Vec<(usize, usize)>> = HashMap::new();
let mut buckets: BTreeMap<(usize, usize), Vec<(usize, usize)>> = BTreeMap::new();
let mut kept: Vec<(usize, usize, f32)> = Vec::new();
for (r, c, s) in cands {
let (br, bc) = (r / sp, c / sp);
@@ -627,8 +654,8 @@ mod tests {
assert!(a.len() <= MAX_ATTRACTORS);
// Sorted by (type as u8, row, col).
for win in a.windows(2) {
let ka = (win[0].attractor_type.clone() as u8, win[0].row, win[0].col);
let kb = (win[1].attractor_type.clone() as u8, win[1].row, win[1].col);
let ka = (win[0].attractor_type as u8, win[0].row, win[0].col);
let kb = (win[1].attractor_type as u8, win[1].row, win[1].col);
assert!(ka <= kb, "attractors must be sorted");
}
}
@@ -668,12 +695,16 @@ mod tests {
// (a global-strength cap would drop all of them — the bug Hoshe caught).
let h = hm(sine_grid(512, 256), 512, 256, 0.40);
let dr = drainage::analyze(&h.data, 512, 256, 0.40);
assert!(!dr.river_network.mouths.is_empty(), "fixture must have mouths");
assert!(
!dr.river_network.mouths.is_empty(),
"fixture must have mouths"
);
let ta = TerrainAnalysis::analyze(&h, &dr);
let a = extract_attractors(&h, &dr, &ta);
assert!(a.len() <= MAX_ATTRACTORS);
assert!(
a.iter().any(|x| x.attractor_type == AttractorType::RiverMouth),
a.iter()
.any(|x| x.attractor_type == AttractorType::RiverMouth),
"RiverMouth attractors must survive the cap when mouths exist"
);
}