diff --git a/server/src/atlas/chunk_context.rs b/server/src/atlas/chunk_context.rs index 27dbd0146..9ba5dedd2 100644 --- a/server/src/atlas/chunk_context.rs +++ b/server/src/atlas/chunk_context.rs @@ -18,11 +18,20 @@ //! `RegionProfile`), not from the chunk-local seed. This is enforced by structure: //! the chunk seed is only used for sub-chunk (<64 m) scatter. //! +//! The same rule places the feature axes themselves (T-1040/T-1041): the channel +//! centreline and landform axis (`channel_anchor_m`) and the coast-face line +//! (`coast_anchor_m`) are **world-metre coordinates derived once per region** +//! from the region-scale seed. Voxel generators measure distance to these +//! anchors in continuous world coordinates — never from the world origin and +//! never folded into the 64 m chunk frame. +//! //! ## D-010 compliance //! //! All derivation is integer arithmetic. The only f64 in this module is -//! `meander_wavelength_m` (positional physics, not a gate comparison), consistent -//! with D-239 §4 ("the warp is position math, not a structural decision"). +//! `meander_wavelength_m` (positional physics, D-239 §4). Structural decisions +//! consume it only via deterministic i32 truncation (the `has_active_channel` +//! band reach), mirroring the established voxel.rs truncate-before-decision +//! convention — only correctly-rounded f64 +,-,*,/ feed the truncation. //! //! ## SeedDomain extension //! @@ -66,6 +75,32 @@ pub enum BasinDirection { /// `BTreeMap` key — implements `Ord` for D-010 determinism. pub type ChunkPos = (i32, i32); +/// Chunk edge length in metres. +pub const CHUNK_M: i32 = 64; + +/// Region edge length in chunks (region ≈ 1 km = 16 chunks). Must match the +/// `>> REGION_CHUNKS_SHIFT` region-index mapping used for the region-scale seed. +const REGION_CHUNKS_SHIFT: u32 = 4; + +/// Region edge length in metres (1 024 m). +const REGION_M: i32 = CHUNK_M << REGION_CHUNKS_SHIFT; + +/// Margin keeping a region's feature anchor away from the region edge, so the +/// channel's full swept band (max meander amplitude wavelength/4 ≈ 162 m + +/// channel edge + levee band + warp bound ≈ 187 m) stays inside the region. +/// Cross-region feature continuity is the stage-2 Voronoi model (T-1040). +const ANCHOR_MARGIN_M: i32 = 192; + +/// Seed-addressable anchor span within a region (REGION_M − 2 × margin). +const ANCHOR_SPAN_M: i32 = REGION_M - 2 * ANCHOR_MARGIN_M; + +/// Maximum levee band width in metres (`voxel::in_levee_band`: 4 + 3 jitter). +const LEVEE_BAND_MAX_M: i32 = 7; + +/// Domain-warp displacement bound in metres (mirrors `domain_warp::WARP_BOUND`, +/// D-239 §4 ±8 m). Integer here — used only to widen the channel gate band. +const WARP_BOUND_M: i32 = 8; + // --------------------------------------------------------------------------- // ChunkContext // --------------------------------------------------------------------------- @@ -82,11 +117,15 @@ pub type ChunkPos = (i32, i32); /// Used by MeanderReach and AlluvialPlain voxel generators to place the channel. /// - `meander_wavelength_m` — meander wavelength in metres. Derived from /// region-level morphology (slope, moisture), seeded at region scale (> 64 m). -/// f64 for positional physics (D-239 §4); not used in any gate comparison. -/// - `has_active_channel` — whether a water channel is present in this chunk, -/// derived from the region's `ocean_fraction_q` and `river_threshold`. +/// f64 for positional physics (D-239 §4); structural decisions consume it +/// only via deterministic i32 truncation (the `has_active_channel` band). +/// - `has_active_channel` — whether a water channel is present in this chunk: +/// the region has water presence AND the channel's swept band around +/// `channel_anchor_m` crosses this chunk (T-1040). /// - `channel_width_m` — channel width in metres (integer; D-010). 0 if no /// active channel. +/// - `channel_anchor_m` / `coast_anchor_m` — region-anchored feature axes in +/// world metres (T-1040/T-1041, D-239 §10). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChunkContext { /// Dominant basin-flow direction for this chunk's drainage catchment. @@ -101,13 +140,28 @@ pub struct ChunkContext { pub meander_wavelength_m: f64, /// Whether this chunk contains an active water channel. - /// True when the region's `ocean_fraction_q` × `river_threshold` signal - /// indicates a perennial waterway crosses this chunk. + /// True when the region has water presence (`ocean_fraction_q` ≥ 10) AND + /// the channel's swept band around `channel_anchor_m` crosses this chunk's + /// cross-axis range (T-1040 — channels exist where the region says, not + /// region-wide and not only at the world origin). pub has_active_channel: bool, /// Active channel width in metres (integer; D-010). 0 if no active channel. /// Derived from region morphology and slope. pub channel_width_m: i32, + + /// Cross-axis world-metre coordinate of the region's feature centreline: + /// channel/meander axis, fjord trough, gorge floor, braid-fan axis. + /// Derived once per region from the region-scale seed (T-1040/T-1041, + /// D-239 §10) — constant across all chunks of a region, so the feature is + /// continuous across chunk boundaries. Cross axis = x for N/S basins, + /// y for E/W basins. + pub channel_anchor_m: i32, + + /// Along-axis (basin-axis) world-metre coordinate of the CliffCoast face + /// line. Region-scale (T-1041): one continuous coast per region, not a + /// 64 m sawtooth. Along axis = y for N/S basins, x for E/W basins. + pub coast_anchor_m: i32, } // --------------------------------------------------------------------------- @@ -155,21 +209,44 @@ pub fn derive_chunk_context( // is positional physics (D-239 §4, not a gate comparison). let meander_wavelength_m = derive_meander_wavelength(region); - // Active channel — present when the region has meaningful water presence - // (ocean_fraction_q >= 10 indicates a perennial waterway or water body - // covers at least 10% of the region). The river_threshold governs D8 - // drainage accumulation at a finer scale; at the chunk level we use - // ocean_fraction_q as the direct proxy for water presence (D-239 §10: - // the chunk carries one basin-direction, not a per-tile flow grid). - let has_active_channel = region.ocean_fraction_q >= 10; - - // Channel width — derived from ocean_fraction_q (proxy for water presence - // at region scale); integer metres; 0 when no active channel. - let channel_width_m = if has_active_channel { - derive_channel_width(region) - } else { - 0 + // Region-anchored feature axes (T-1040/T-1041, D-239 §10): channel and + // landform centrelines have wavelength > 64 m, so their position derives + // from the region-scale seed — never the chunk frame or the world origin. + // Cross axis ⊥ basin_direction (x for N/S, y for E/W); along axis ∥ basin. + let (cross_chunk, along_chunk) = match basin_direction { + BasinDirection::North | BasinDirection::South => (chunk_pos.0, chunk_pos.1), + BasinDirection::East | BasinDirection::West => (chunk_pos.1, chunk_pos.0), }; + let channel_anchor_m = derive_region_anchor(cross_chunk, (region_seed.seed() >> 16) & 0xFFFF); + let coast_anchor_m = derive_region_anchor(along_chunk, (region_seed.seed() >> 32) & 0xFFFF); + + // Active channel — water presence (ocean_fraction_q >= 10 indicates a + // perennial waterway or water body covers at least 10% of the region) AND + // the channel's swept band around the region anchor crosses this chunk + // (T-1040). The river_threshold governs D8 drainage accumulation at a finer + // scale; at the chunk level ocean_fraction_q is the direct proxy for water + // presence (D-239 §10: one basin-direction, not a per-tile flow grid). + // + // The band is generous (it must cover every chunk that can contain channel, + // levee, or warped-channel voxels — a gate-off chunk renders dry), using + // the larger MeanderReach amplitude (wavelength/4) for both channel families. + let channel_width = derive_channel_width(region); + let has_active_channel = region.ocean_fraction_q >= 10 && { + let wavelength_i = (meander_wavelength_m as i32).max(10); + let amplitude_max = (wavelength_i / 4).max(3); + let edge_max = (channel_width / 2).max(2) + 3; // half-width + max edge jitter + // Floor: the BraidedDelta belt reaches anchor ±(32 thread-centre + 4 + // thread-half) before warp regardless of wavelength — the band must + // cover it even at the short-wavelength extreme (costs ≤4 m of extra + // gate generosity for the other families). + let reach = (amplitude_max + edge_max + LEVEE_BAND_MAX_M).max(32 + 4) + WARP_BOUND_M; + let cross_lo = cross_chunk * CHUNK_M; + let cross_hi = cross_lo + CHUNK_M - 1; + cross_lo <= channel_anchor_m + reach && cross_hi >= channel_anchor_m - reach + }; + + // Channel width — integer metres; 0 when no active channel in this chunk. + let channel_width_m = if has_active_channel { channel_width } else { 0 }; ChunkContext { basin_direction, @@ -177,9 +254,26 @@ pub fn derive_chunk_context( meander_wavelength_m, has_active_channel, channel_width_m, + channel_anchor_m, + coast_anchor_m, } } +/// World-metre anchor coordinate for a region-scale feature axis on one axis. +/// +/// The anchor sits in `[region_origin + ANCHOR_MARGIN_M, region_origin + +/// REGION_M − ANCHOR_MARGIN_M)` so the feature's full swept band stays inside +/// its region (no cross-region band spill; region-seam continuity is the +/// stage-2 Voronoi model, T-1040). Same value for every chunk of the region: +/// the region index is `axis_chunk >> REGION_CHUNKS_SHIFT` (arithmetic shift = +/// floor division, correct for negative chunks) and `seed_bits` comes from the +/// shared region-scale seed. Integer arithmetic (D-010). +fn derive_region_anchor(axis_chunk: i32, seed_bits: u64) -> i32 { + let region_idx = axis_chunk >> REGION_CHUNKS_SHIFT; + let origin_m = region_idx * REGION_M; + origin_m + ANCHOR_MARGIN_M + (seed_bits % ANCHOR_SPAN_M as u64) as i32 +} + /// Fold `(x, y)` chunk coordinates into a single u64 id for seed derivation. /// /// Mirrors `domain_warp::pos_to_id` — zigzag-encode + Cantor pairing. @@ -294,6 +388,17 @@ mod tests { } } + /// The chunk of region (0, 0) whose cross-range contains the region's + /// channel anchor — guaranteed inside the T-1040 channel band. + fn anchor_chunk_pos(world_seed: u64, body_id: &str, region: &RegionProfile) -> ChunkPos { + let probe = derive_chunk_context(world_seed, body_id, region, (0, 0)); + let idx = probe.channel_anchor_m.div_euclid(CHUNK_M); + match probe.basin_direction { + BasinDirection::North | BasinDirection::South => (idx, 0), + BasinDirection::East | BasinDirection::West => (0, idx), + } + } + #[test] fn derive_chunk_context_is_deterministic() { let region = alluvial_region(); @@ -321,14 +426,80 @@ mod tests { #[test] fn alluvial_region_has_active_channel() { let region = alluvial_region(); - let ctx = derive_chunk_context(42, "GJ1c", ®ion, (5, 5)); - // ocean_fraction_q=15 + temperate precip → should have active channel. + // T-1040: the channel is region-anchored — the chunk under the anchor + // must claim it (ocean_fraction_q=15 → water present). + let pos = anchor_chunk_pos(42, "GJ1c", ®ion); + let ctx = derive_chunk_context(42, "GJ1c", ®ion, pos); assert!( ctx.has_active_channel, - "alluvial plain with ocean_fraction_q=15 should have active channel" + "anchor-covering chunk of a watered region must have active channel" ); } + #[test] + fn chunk_outside_channel_band_has_no_active_channel() { + // T-1040: channels are region-anchored, not region-wide. A chunk whose + // cross-range lies outside the channel's swept band (max reach < 192 m + // = 3 chunks) must not claim a channel — pre-fix every chunk of a + // watered region did, while voxels rendered dry floodplain. + let region = alluvial_region(); + let (anchor_pos, probe) = { + let pos = anchor_chunk_pos(42, "GJ1c", ®ion); + (pos, derive_chunk_context(42, "GJ1c", ®ion, pos)) + }; + // 8 cross-chunks away (512 m) is past any band reach but still inside + // region (0, 0) — the anchor margin keeps the anchor chunk in [3, 12]. + let anchor_idx = anchor_pos.0.max(anchor_pos.1); + let far_idx = if anchor_idx < 8 { + anchor_idx + 8 + } else { + anchor_idx - 8 + }; + let far_pos = match probe.basin_direction { + BasinDirection::North | BasinDirection::South => (far_idx, 0), + BasinDirection::East | BasinDirection::West => (0, far_idx), + }; + let far_ctx = derive_chunk_context(42, "GJ1c", ®ion, far_pos); + assert!( + !far_ctx.has_active_channel, + "chunk {far_pos:?} outside the channel band must not claim a channel" + ); + assert_eq!( + far_ctx.channel_width_m, 0, + "no active channel → channel_width_m must be 0" + ); + // Region-scale params stay constant across the region's chunks. + assert_eq!(far_ctx.channel_anchor_m, probe.channel_anchor_m); + assert_eq!(far_ctx.meander_phase, probe.meander_phase); + } + + #[test] + fn anchors_constant_within_region_and_inside_it() { + // T-1040/T-1041: feature anchors are a region property — identical for + // every chunk of the region, and positioned inside the region's extent. + let region = alluvial_region(); + let base = derive_chunk_context(42, "GJ1c", ®ion, (0, 0)); + for pos in [(1, 0), (0, 1), (15, 15), (7, 12)] { + let ctx = derive_chunk_context(42, "GJ1c", ®ion, pos); + assert_eq!( + ctx.channel_anchor_m, base.channel_anchor_m, + "channel anchor must be region-constant (chunk {pos:?})" + ); + assert_eq!( + ctx.coast_anchor_m, base.coast_anchor_m, + "coast anchor must be region-constant (chunk {pos:?})" + ); + } + // Region (0, 0) spans [0, 1024) m on both axes. + assert!((0..1024).contains(&base.channel_anchor_m)); + assert!((0..1024).contains(&base.coast_anchor_m)); + // A different region derives its anchors inside its own extent. + let far = derive_chunk_context(42, "GJ1c", ®ion, (1000, -750)); + assert!((62 * 1024..63 * 1024).contains(&far.channel_anchor_m) || (-47 * 1024..-46 * 1024).contains(&far.channel_anchor_m), + "far region anchor {} must lie inside its region extent (cross axis depends on basin direction)", + far.channel_anchor_m); + } + #[test] fn dry_region_has_no_active_channel() { let region = RegionProfile { @@ -353,9 +524,11 @@ mod tests { #[test] fn channel_width_in_game_feel_range() { - // D-239 §9: river crossings 3–15 m. + // D-239 §9: river crossings 3–15 m. Measured on a chunk that carries + // the channel (T-1040 gating zeroes the width elsewhere). let region = alluvial_region(); - let ctx = derive_chunk_context(42, "GJ1c", ®ion, (5, 5)); + let pos = anchor_chunk_pos(42, "GJ1c", ®ion); + let ctx = derive_chunk_context(42, "GJ1c", ®ion, pos); assert!( (3..=15).contains(&ctx.channel_width_m), "channel_width_m {} out of game-feel range [3, 15]", diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index 5a5d647a2..9606f499d 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -413,10 +413,11 @@ pub fn derive_voxel_column( /// /// ## Meander channel placement /// -/// The channel is placed by a sine-wave approximation centred on the chunk's -/// `basin_direction`. The perpendicular distance from the voxel to the wave -/// determines whether the voxel is in-channel. This avoids per-tile flow -/// grids while producing a spatially coherent channel (D-239 §10). +/// The channel is a sine-wave approximation running along `basin_direction`, +/// centred on the region-anchored `channel_anchor_m` (T-1040). The +/// perpendicular distance from the voxel to the wave determines whether the +/// voxel is in-channel. This avoids per-tile flow grids while producing a +/// spatially coherent channel continuous across chunk boundaries (D-239 §10). /// /// Channel width is `chunk.channel_width_m` (game-feel range 3–15 m). fn generate_alluvial_plain( @@ -570,8 +571,9 @@ fn generate_lava_field( /// - Valley floor is narrow (chokepoint — D-239 §9) with `Deep` water. /// - Walls rise steeply; rocky, very little vegetation. /// -/// The fjord inlet occupies the centre of the chunk (along `basin_direction`). -/// Cross-section: wall | moraine | Deep water | moraine | wall. +/// The fjord trough runs along `basin_direction` at the region-anchored +/// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE valley spans +/// the region's chunks. Cross-section: wall | moraine | Deep water | moraine | wall. fn generate_fjord_wall( region: &RegionProfile, chunk: &ChunkContext, @@ -580,8 +582,7 @@ fn generate_fjord_wall( ) -> VoxelColumn { // ── Cross-valley coordinate ─────────────────────────────────────────── // Fjord runs along basin_direction; cross = perpendicular distance from - // the centre-line. We fold the absolute cross-distance to get a positive - // distance in [0, 32] (chunk half-width = 32 m). + // the centre-line. let (along, cross) = match chunk.basin_direction { crate::atlas::chunk_context::BasinDirection::North | crate::atlas::chunk_context::BasinDirection::South => (voxel_pos.1, voxel_pos.0), @@ -589,12 +590,10 @@ fn generate_fjord_wall( | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; - // Cross-channel absolute distance from centreline (fold into positive). - // We use modulo 64 to map the absolute coordinate into the chunk frame. - let cross_from_centre = { - let rel = cross.rem_euclid(64) - 32; // −32..+31 - rel.abs() // 0..32 - }; + // Cross-channel distance from the region-anchored trough centreline + // (T-1041): continuous world coordinates — folding into the 64 m chunk + // frame repeated a complete fjord in every chunk (D-239 §10 violation). + let cross_from_centre = (cross - chunk.channel_anchor_m).abs(); // ── U-valley geometry ───────────────────────────────────────────────── // The fjord bottom is 4–8 m wide (chokepoint, D-239 §9 "gorge floors 2–8 m"). @@ -646,8 +645,11 @@ fn generate_fjord_wall( // Rock→vertical faces (D-239 §8). Elevation rises steeply from // moraine_outer to the wall top. let wall_dist = cross_from_centre - moraine_outer; - // Steep linear rise: 6 m per metre of cross-distance (approximates vertical). - let wall_rise = wall_dist * 6; + // Steep linear rise: 6 m per metre of cross-distance (approximates + // vertical), saturating at a 120 m plateau 20 m out — cross distance + // is region-scale post-T-1041, so the rise must not grow unbounded to + // the region edge (clamp before multiplying: i32 overflow otherwise). + let wall_rise = wall_dist.min(20) * 6; // Cirques (grade ≥ 2): occasional hollowed pocket (2–4 m depression) at // upper wall. seed-gated, ~1-in-8 frequency. let cirque_depression = if glacier_grade >= 2 && (sub_chunk_seed >> 6) & 0x7 == 0 { @@ -696,8 +698,10 @@ fn generate_fjord_wall( /// Vertical rock face at the coastal water edge: /// - `Rock` terrain (D-239 §8 Rock→vertical faces law). /// - Sharp elevation drop to `Shallow`/`Deep` water at the base. -/// - The coast runs perpendicular to `basin_direction`; inland is high, -/// seaward is near-zero. +/// - The coast face sits on the region-anchored line `chunk.coast_anchor_m` +/// along the seaward axis (T-1041, D-239 §10): ONE continuous coast line +/// per region (warp-displaced), not a 64 m sawtooth. Inland of the line is +/// high ground; seaward is ocean. /// - Narrow ledge/platform at the cliff base (passable ground, Shallow). /// - Virtually no flat ground (D-239 §8 — Rock→vertical, not terraced). fn generate_cliff_coast( @@ -707,25 +711,21 @@ fn generate_cliff_coast( sub_chunk_seed: u64, ) -> VoxelColumn { // ── Coast orientation ───────────────────────────────────────────────── - // basin_direction points seaward (water flows to ocean). The seaward axis - // is the "along-flow" direction; cross = coast face direction. - // Cross-coordinate from the chunk centre in [−32, +31]. - let seaward_coord = match chunk.basin_direction { - crate::atlas::chunk_context::BasinDirection::North => -voxel_pos.1, - crate::atlas::chunk_context::BasinDirection::South => voxel_pos.1, - crate::atlas::chunk_context::BasinDirection::East => voxel_pos.0, - crate::atlas::chunk_context::BasinDirection::West => -voxel_pos.0, + // basin_direction points seaward (water flows to ocean). coast_d is the + // signed seaward distance from the region-anchored coast line in + // continuous world metres (T-1041): negative = inland, positive = seaward. + let coast_d = match chunk.basin_direction { + crate::atlas::chunk_context::BasinDirection::North => chunk.coast_anchor_m - voxel_pos.1, + crate::atlas::chunk_context::BasinDirection::South => voxel_pos.1 - chunk.coast_anchor_m, + crate::atlas::chunk_context::BasinDirection::East => voxel_pos.0 - chunk.coast_anchor_m, + crate::atlas::chunk_context::BasinDirection::West => chunk.coast_anchor_m - voxel_pos.0, }; - // Normalise into chunk frame [0, 63] with seaward = 63. - let seaward_norm = (seaward_coord.rem_euclid(64)).clamp(0, 63); - // ── Cliff face ──────────────────────────────────────────────────────── - // The cliff face is at seaward_norm ≈ 48–55 (roughly 3/4 across the chunk). - // Inland (0..47): high ground, rocky top. - // Face (48..55): steep drop (>6 m per metre — near-vertical, D-239 §8). - // Ledge (56..60): rocky beach platform, Shallow. - // Ocean (61..63): Deep water. + // Inland (coast_d < 0): high ground, rocky top. + // Face (0..8): steep drop (>6 m per metre — near-vertical, D-239 §8). + // Ledge (8..13): rocky beach platform, Shallow. + // Ocean (≥13): Deep water. // // Cliff height derived from region elevation. let cliff_top_elev = (region.elev_q / 2).max(15); // at least 15 m cliff @@ -735,18 +735,18 @@ fn generate_cliff_coast( // Ledge variation noise: bits [3:5] → 0–2 m. let ledge_noise = ((sub_chunk_seed >> 3) & 0x3) as i32; - let (elevation_m, water) = if seaward_norm < 48 { + let (elevation_m, water) = if coast_d < 0 { // Inland high ground — rocky plateau. let inland_noise = ((sub_chunk_seed >> 5) & 0xF) as i32 - 7; // [−7, +8] let elev = (cliff_top_elev + inland_noise).max(cliff_top_elev / 4); (elev, Water::Dry) - } else if seaward_norm < 56 { + } else if coast_d < 8 { // Cliff face — steep drop. Each metre seaward drops ~cliff_top/8 metres. - let face_dist = seaward_norm - 48; // 0–7 + let face_dist = coast_d; // 0–7 let drop = (face_dist * cliff_top_elev) / 8; let elev = (cliff_top_elev - drop + face_noise).max(1); (elev, Water::Dry) - } else if seaward_norm < 61 { + } else if coast_d < 13 { // Rocky ledge / splash zone. let elev = (1 + ledge_noise).max(0); (elev, Water::Shallow) @@ -755,17 +755,19 @@ fn generate_cliff_coast( (0, Water::Deep) }; - // Vegetation: nearly barren on rock faces; tiny scatter of Scrub on inland top. + // Vegetation: nearly barren on rock faces; tiny scatter of Scrub on the + // inland top (≥18 m / ≥28 m inland of the face — same offsets as the old + // chunk-frame layout, now measured from the region-anchored coast line). let vegetation = match region.vegetation_class { VegetationClass::Forest | VegetationClass::RiparianThicket => { - if seaward_norm < 30 && (sub_chunk_seed >> 10) & 0x7 < 3 { + if coast_d < -18 && (sub_chunk_seed >> 10) & 0x7 < 3 { Vegetation::Scrub } else { Vegetation::Barren } } VegetationClass::Scrub | VegetationClass::RiparianScrub => { - if seaward_norm < 20 && (sub_chunk_seed >> 10) & 0x3 == 0 { + if coast_d < -28 && (sub_chunk_seed >> 10) & 0x3 == 0 { Vegetation::Scrub } else { Vegetation::Barren @@ -795,9 +797,12 @@ fn generate_cliff_coast( /// - Multiple shallow distributary channels (NOT single-thread meander). /// - Very low elevation — near sea level (D-239 §8 drainage monotonicity: /// mouths at sea level). -/// - Channel determination: 3 independent braided threads, each offset by -/// ~1/3 of the chunk width and width 3–8 m. Thread presence is seeded at -/// region scale (meander_phase encodes thread offsets). +/// - Channel determination: 3 independent braided threads anastomosing +/// across a fan belt centred on the region-anchored axis +/// `chunk.channel_anchor_m` (T-1041, D-239 §10), each thread 3–8 m wide. +/// Thread offsets are seeded at region scale (meander_phase encodes them), +/// so the threads run continuously across the region's chunks instead of +/// restarting every 64 m. /// - `Shallow` in-channel; `Dry` on gravel bars between threads. fn generate_braided_delta( region: &RegionProfile, @@ -821,11 +826,9 @@ fn generate_braided_delta( | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; - // Map cross into chunk frame [0, 63]. - let cross_norm = cross.rem_euclid(64); - - // Three braided threads with well-separated centres derived from meander_phase. - // Thread centres spread across the chunk width (D-239 §8 Gravel→braided). + // Three braided threads with well-separated centres derived from meander_phase, + // spread across a 64 m fan belt centred on the region-anchored axis + // (T-1041 — world cross coordinates, constant for all chunks of the region). // Each centre uses independent bit-mixing (splitmix64-style) so threads are // provably separated regardless of phase value — no correlated bit-slices. let phase_u64 = chunk.meander_phase as u64; @@ -851,21 +854,19 @@ fn generate_braided_delta( h ^ (h >> 31) }; let thread_centres: [i32; 3] = [ - (mix_a % 64) as i32, - (mix_b % 64) as i32, - (mix_c % 64) as i32, + chunk.channel_anchor_m - 32 + (mix_a % 64) as i32, + chunk.channel_anchor_m - 32 + (mix_b % 64) as i32, + chunk.channel_anchor_m - 32 + (mix_c % 64) as i32, ]; // Thread width: 3–6 m half-width derived from channel_width_m. let thread_half = (chunk.channel_width_m / 2).clamp(2, 4); - // Check if voxel is in any braided thread. - // True modular distance on a [0, 64) ring — handles wrap at both edges. - let in_any_thread = thread_centres.iter().any(|¢re| { - let d = (cross_norm - centre).rem_euclid(64); - let dist = d.min(64 - d); - dist <= thread_half - }); + // Check if voxel is in any braided thread — continuous world-coordinate + // distance (the old [0, 64) ring fold restarted the threads every chunk). + let in_any_thread = thread_centres + .iter() + .any(|¢re| (cross - centre).abs() <= thread_half); let (elevation_m, water) = if in_any_thread { // In a distributary channel — at or just below base elevation. @@ -1036,6 +1037,9 @@ fn generate_dune_strand( /// - Massive elevation differential: walls are 20–60 m above the floor. /// - `Shallow` water in the incised channel at the floor. /// - Cross-section: wall | wall | narrow floor (with channel) | wall | wall. +/// - The gorge runs along `basin_direction` at the region-anchored +/// centreline `chunk.channel_anchor_m` (T-1041, D-239 §10): ONE gorge +/// spans the region's chunks. fn generate_incised_gorge( region: &RegionProfile, chunk: &ChunkContext, @@ -1050,11 +1054,10 @@ fn generate_incised_gorge( | crate::atlas::chunk_context::BasinDirection::West => (voxel_pos.0, voxel_pos.1), }; - // Distance from gorge centre-line in [0, 32]. - let cross_from_centre = { - let rel = cross.rem_euclid(64) - 32; // −32..+31 - rel.abs() - }; + // Distance from the region-anchored gorge centreline (T-1041): continuous + // world coordinates — folding into the 64 m chunk frame carved a gorge in + // every chunk (D-239 §10 violation). + let cross_from_centre = (cross - chunk.channel_anchor_m).abs(); // ── Gorge floor width (D-239 §9: 2–8 m) ────────────────────────────── // Floor half-width: 1–4 m (total 2–8 m). Derived from channel_width_m @@ -1083,8 +1086,11 @@ fn generate_incised_gorge( // Elevation rises steeply: each metre of cross-distance from floor edge // adds wall_step metres. We model this as a steep ramp. let wall_dist = cross_from_centre - floor_half; - // Steep wall: 8 m rise per metre of wall distance (near-vertical, §8 Rock law). - let wall_rise = wall_dist * 8; + // Steep wall: 8 m rise per metre of wall distance (near-vertical, §8 + // Rock law). The rise saturates at the wall_top clamp below; clamp the + // region-scale distance (T-1041) before multiplying — i32 overflow at + // extreme coordinates otherwise. + let wall_rise = wall_dist.min(16) * 8; let elev = (floor_elev + wall_rise + wall_noise).clamp(floor_elev, wall_top); (elev, Water::Dry) }; @@ -1580,7 +1586,8 @@ fn compute_meander_reach_channel( amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1) }; - let perp_distance = (cross - meander_displacement).abs(); + // Region-anchored centreline (T-1040) — same anchor as AlluvialPlain. + let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); // Edge jitter from sub-chunk seed (±1 m), same as AlluvialPlain. let edge_jitter = (sub_chunk_seed & 0x3) as i32; @@ -1621,7 +1628,8 @@ fn in_levee_band(voxel_pos: VoxelPos, chunk: &ChunkContext, sub_chunk_seed: u64) amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl.max(1) }; - let perp_distance = (cross - meander_displacement).abs(); + // Region-anchored centreline (T-1040) — same anchor as the channel itself. + let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); let edge_jitter = (sub_chunk_seed & 0x3) as i32; let half_width = (chunk.channel_width_m / 2).max(2); @@ -1690,8 +1698,10 @@ fn compute_channel_state( amplitude - (2 * amplitude * (angle_mod - half_wl)) / half_wl }; - // Perpendicular distance from voxel to channel centreline. - let perp_distance = (cross - meander_displacement).abs(); + // Perpendicular distance from voxel to the region-anchored channel + // centreline (T-1040): the channel axis is `chunk.channel_anchor_m` on the + // cross axis — a region-scale world coordinate, never the world origin. + let perp_distance = (cross - chunk.channel_anchor_m - meander_displacement).abs(); // Sub-chunk noise: ±1 m jitter on the channel edge (wavelength < 64 m). // Use low bits of seed for integer jitter — D-010. @@ -1935,7 +1945,7 @@ pub const DEFAULT_VOXEL_CACHE_CAPACITY: usize = 8192; #[cfg(test)] mod tests { use super::*; - use crate::atlas::chunk_context::derive_chunk_context; + use crate::atlas::chunk_context::{derive_chunk_context, BasinDirection}; use crate::atlas::region_profile::{ GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass, }; @@ -1944,6 +1954,29 @@ mod tests { // Test fixtures // ----------------------------------------------------------------------- + /// The ChunkContext of the region-(0,0) chunk whose cross-range contains + /// the channel anchor — guaranteed inside the T-1040 channel band. + /// Feature placement is region-anchored, so tests sample around + /// `chunk.channel_anchor_m` instead of assuming chunk-frame positions. + fn anchor_chunk(seed: u64, body: &str, region: &RegionProfile) -> ChunkContext { + let probe = derive_chunk_context(seed, body, region, (0, 0)); + let idx = probe.channel_anchor_m.div_euclid(64); + let pos = match probe.basin_direction { + BasinDirection::North | BasinDirection::South => (idx, 0), + BasinDirection::East | BasinDirection::West => (0, idx), + }; + derive_chunk_context(seed, body, region, pos) + } + + /// Map a (cross, along) coordinate pair to (tile_x, tile_y) for the + /// chunk's basin axes — cross ⊥ basin_direction, along ∥ basin_direction. + fn cross_along_to_tile(chunk: &ChunkContext, cross: i32, along: i32) -> (i32, i32) { + match chunk.basin_direction { + BasinDirection::North | BasinDirection::South => (cross, along), + BasinDirection::East | BasinDirection::West => (along, cross), + } + } + fn alluvial_region() -> RegionProfile { RegionProfile { morphology_zone: MorphologyZone::AlluvialPlain, @@ -2340,33 +2373,19 @@ mod tests { #[test] fn fjord_wall_centre_has_deep_water() { - // The fjord inlet (centreline of the chunk) should have Deep water. + // The fjord trough (region-anchored centreline, T-1041) must have Deep + // water. Sample a small window around the anchor — the domain warp can + // displace any single tile off the 4–8 m floor. let region = fjord_region(); - // Use a chunk centred around x=0, basin direction North/South. - // The fjord centre is at cross = 0 relative to chunk centre. - // Cross coordinate = x, so at x=32 (the mid-chunk point in [0,63]). - // We sample at tile_x=32 to hit the centre. let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); - // Guard: this sweep assumes N/S basin (cross axis = tile_x). If the seed - // yields E/W the test would sweep the wrong axis and pass vacuously. + let anchor = chunk.channel_anchor_m; + let any_deep = (anchor - 4..=anchor + 4).any(|c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 50); + derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty).water == Water::Deep + }); assert!( - matches!( - chunk.basin_direction, - crate::atlas::chunk_context::BasinDirection::North - | crate::atlas::chunk_context::BasinDirection::South - ), - "test assumes N/S basin direction; pick a different seed if this fires" - ); - // Find a position near the fjord centreline. We pick x positions that - // map to cross_from_centre = 0 (chunk centre). At x=0 in the grid, - // rem_euclid(64) - 32 = -32 → abs=32. At x=32, rem_euclid(64)=32, 32-32=0. - // So tile_x near 32 (in basin_direction perpendicular axis for North). - // For North/South basin direction: cross = voxel_pos.0. - let col_centre = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, 32, 50); - assert_eq!( - col_centre.water, - Water::Deep, - "FjordWall centreline must have Deep water (the fjord inlet)" + any_deep, + "FjordWall centreline (anchor {anchor}) must have Deep water (the fjord inlet)" ); } @@ -2375,24 +2394,23 @@ mod tests { // Wall elevation must be substantially higher than fjord floor. let region = fjord_region(); let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); - // Guard: this test assumes N/S basin (cross axis = tile_x). + let anchor = chunk.channel_anchor_m; + // Floor: the lowest tile in the trough window around the anchor. + let floor_elev = (anchor - 4..=anchor + 4) + .map(|c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 50); + derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty).elevation_m + }) + .min() + .unwrap(); + // Wall: 30 m off the centreline (past the moraine band at ≤14 m). + let (wx, wy) = cross_along_to_tile(&chunk, anchor - 30, 50); + let wall_col = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, wx, wy); assert!( - matches!( - chunk.basin_direction, - crate::atlas::chunk_context::BasinDirection::North - | crate::atlas::chunk_context::BasinDirection::South - ), - "test assumes N/S basin direction; pick a different seed if this fires" - ); - // Floor at centre (x=32 for North/South basin). - let floor_col = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, 32, 50); - // Wall far from centre (x = 0 or x = 63 = cross_from_centre ≥ 32). - let wall_col = derive_voxel_column(42, "Fjordheim", ®ion, &chunk, 0, 50); - assert!( - wall_col.elevation_m > floor_col.elevation_m, + wall_col.elevation_m > floor_elev, "FjordWall walls ({}) must be higher than the floor ({})", wall_col.elevation_m, - floor_col.elevation_m + floor_elev ); } @@ -2422,21 +2440,15 @@ mod tests { // D-239 §9: fjord floor (Deep water) must be 2–8 m wide across a cross-section. // With floor_half = glacier_grade.clamp(2,4) and grade ≥ 2 (upstream gate), // total Deep-water width = 2×floor_half + noise ∈ [4, 8] m. + // Cross-section spans the region-anchored trough (T-1041). let region = fjord_region(); // glaciation_grade = Moderate (= 2) let chunk = derive_chunk_context(42, "Fjordheim", ®ion, (0, 0)); - // Guard: sweep assumes N/S basin (cross axis = tile_x). - assert!( - matches!( - chunk.basin_direction, - crate::atlas::chunk_context::BasinDirection::North - | crate::atlas::chunk_context::BasinDirection::South - ), - "test assumes N/S basin direction; pick a different seed if this fires" - ); - // Count Deep-water tiles across the 64-tile cross-section. - let deep_tiles = (0..64i32) - .filter(|&x| { - derive_voxel_column(42, "Fjordheim", ®ion, &chunk, x, 50).water == Water::Deep + let anchor = chunk.channel_anchor_m; + // Count Deep-water tiles across a 64-tile cross-section around the anchor. + let deep_tiles = (anchor - 32..anchor + 32) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 50); + derive_voxel_column(42, "Fjordheim", ®ion, &chunk, tx, ty).water == Water::Deep }) .count(); assert!( @@ -2480,15 +2492,17 @@ mod tests { fn cliff_coast_steep_elevation_drop_near_water() { // Verify the CliffCoast structural property: there must be tiles with Deep // water AND there must be a significant elevation range (high ground + ocean). - // We don't hard-code which tile_x is "inland" vs "ocean" because basin_direction - // and domain warp interact — instead we sample a full transect and verify the - // property that: (a) some tiles have Deep water, (b) the max elevation in - // the transect exceeds the ocean elevation by > 10 m (the "vertical face" law). + // The coast face sits on the region-anchored line `coast_anchor_m` along + // the seaward (basin) axis (T-1041) — sample a transect across it. let region = cliff_region(); let chunk = derive_chunk_context(42, "Velen", ®ion, (0, 0)); - // Sample 64 cross-coast positions at a fixed along-coast coordinate. - let cols: Vec = (0..64i32) - .map(|x| derive_voxel_column(42, "Velen", ®ion, &chunk, x, 50)) + let coast = chunk.coast_anchor_m; + // Sample 64 positions along the seaward axis, crossing the coast line. + let cols: Vec = (coast - 32..coast + 32) + .map(|a| { + let (tx, ty) = cross_along_to_tile(&chunk, 50, a); + derive_voxel_column(42, "Velen", ®ion, &chunk, tx, ty) + }) .collect(); let has_deep = cols.iter().any(|c| c.water == Water::Deep); let max_elev = cols.iter().map(|c| c.elevation_m).max().unwrap(); @@ -2577,18 +2591,22 @@ mod tests { #[test] fn braided_delta_has_multiple_shallow_channels() { // Gravel→braided (D-239 §8): NOT single-thread. Multiple channels means - // a range of positions across the chunk width should contain some Shallow water. + // a range of positions across the fan belt should contain some Shallow + // water. The belt is centred on the region-anchored fan axis (T-1041). let region = delta_region(); - let chunk = derive_chunk_context(42, "delta_body", ®ion, (0, 0)); - // Sample 64 positions across the chunk width (cross-section). - let shallow_count = (0..64i32) - .filter(|&x| { - derive_voxel_column(42, "delta_body", ®ion, &chunk, x, 100).water + let chunk = anchor_chunk(42, "delta_body", ®ion); + let anchor = chunk.channel_anchor_m; + // Sample 80 positions across the fan belt (anchor ± 40 covers the + // 64 m belt plus thread width). + let shallow_count = (anchor - 40..anchor + 40) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 100); + derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty).water == Water::Shallow }) .count(); // Three braided threads, each 3–8 m wide → expect at least 6 shallow tiles - // in a 64-tile transect. + // in the belt transect. assert!( shallow_count >= 6, "BraidedDelta must have multiple braided channels; only {shallow_count} shallow in transect" @@ -2602,9 +2620,13 @@ mod tests { // We verify by checking that Shallow water appears in at least two // disjoint groups separated by Dry ground. let region = delta_region(); - let chunk = derive_chunk_context(42, "delta_body", ®ion, (0, 0)); - let waters: Vec = (0..64i32) - .map(|x| derive_voxel_column(42, "delta_body", ®ion, &chunk, x, 100).water) + let chunk = anchor_chunk(42, "delta_body", ®ion); + let anchor = chunk.channel_anchor_m; + let waters: Vec = (anchor - 40..anchor + 40) + .map(|c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 100); + derive_voxel_column(42, "delta_body", ®ion, &chunk, tx, ty).water + }) .collect(); // Count Dry→Shallow transitions = number of channel entrances. let transitions = waters @@ -2766,25 +2788,15 @@ mod tests { fn incised_gorge_floor_width_in_spec() { // D-239 §9: gorge floors 2–8 m wide. We verify by checking that the // total width of the Shallow water zone (the incised channel on the floor) - // across a cross-section is within [2, 8] m. + // across a cross-section is within [2, 8] m. Cross-section spans the + // region-anchored gorge centreline (T-1041). let region = gorge_region(); let chunk = derive_chunk_context(42, "gorge_body", ®ion, (0, 0)); - // Guard: this sweep assumes N/S basin (cross axis = tile_x). If the seed - // yields E/W the sweep would be along the gorge, not across it. - assert!( - matches!( - chunk.basin_direction, - crate::atlas::chunk_context::BasinDirection::North - | crate::atlas::chunk_context::BasinDirection::South - ), - "test assumes N/S basin direction; pick a different seed if this fires" - ); - // Sample 64 cross-positions at a fixed along-axis position. - // For North/South basin direction: cross = tile_x. - // The floor should be ≤ 8 tiles (= 8 m) wide. - let floor_tiles = (0..64i32) - .filter(|&x| { - derive_voxel_column(42, "gorge_body", ®ion, &chunk, x, 100).water + let anchor = chunk.channel_anchor_m; + let floor_tiles = (anchor - 32..anchor + 32) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 100); + derive_voxel_column(42, "gorge_body", ®ion, &chunk, tx, ty).water == Water::Shallow }) .count(); @@ -2799,15 +2811,24 @@ mod tests { // Walls must be substantially higher than the gorge floor. let region = gorge_region(); let chunk = derive_chunk_context(42, "gorge_body", ®ion, (0, 0)); - // Floor: tile at cross_from_centre = 0, i.e. tile_x = 32. - let floor_col = derive_voxel_column(42, "gorge_body", ®ion, &chunk, 32, 100); - // Wall: tile at cross_from_centre = 30, i.e. tile_x = 62 or tile_x = 2. - let wall_col = derive_voxel_column(42, "gorge_body", ®ion, &chunk, 62, 100); + let anchor = chunk.channel_anchor_m; + // Floor: the lowest tile in the trough window around the anchor + // (the warp can displace any single tile off the 2–8 m floor). + let floor_elev = (anchor - 4..=anchor + 4) + .map(|c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 100); + derive_voxel_column(42, "gorge_body", ®ion, &chunk, tx, ty).elevation_m + }) + .min() + .unwrap(); + // Wall: 30 m off the centreline. + let (wx, wy) = cross_along_to_tile(&chunk, anchor + 30, 100); + let wall_col = derive_voxel_column(42, "gorge_body", ®ion, &chunk, wx, wy); assert!( - wall_col.elevation_m > floor_col.elevation_m + 10, + wall_col.elevation_m > floor_elev + 10, "IncisedGorge walls ({}) must be >> gorge floor ({})", wall_col.elevation_m, - floor_col.elevation_m + floor_elev ); } @@ -2906,18 +2927,21 @@ mod tests { #[test] fn meander_reach_has_active_channel() { - // MeanderReach must have a channel with Shallow/Deep water somewhere in a transect. + // MeanderReach must have a channel with Shallow/Deep water somewhere in + // a transect across the region-anchored channel band (T-1040). let region = meander_region(); - let chunk = derive_chunk_context(42, "meander_body", ®ion, (0, 0)); - // The chunk has_active_channel = true (ocean_fraction_q=20 >= 10). + let chunk = anchor_chunk(42, "meander_body", ®ion); + // The anchor-covering chunk has an active channel (ocean_fraction_q=20 >= 10). assert!( chunk.has_active_channel, - "MeanderReach test chunk must have active channel" + "MeanderReach anchor chunk must have active channel" ); + let anchor = chunk.channel_anchor_m; // Sample a wide cross-section and find at least one wet tile. - let wet_count = (-200..200i32) - .filter(|&x| { - let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, x, 50); + let wet_count = (anchor - 200..anchor + 200) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&chunk, c, 50); + let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, ty); col.water != Water::Dry }) .count(); @@ -2929,31 +2953,34 @@ mod tests { #[test] fn meander_reach_channel_below_levee() { - // D-239 §9 ElevationDelta: channel tiles must have lower elevation than - // off-channel tiles in the vicinity. + // D-239 §9 ElevationDelta: channel tiles must sit below off-channel + // tiles in the vicinity. Per-voxel micro-relief is ±4 m, so a single + // wet/dry tile pair is noise — compare the channel bottom (min wet) + // against the floodplain/levee top (max dry) across the transect. let region = meander_region(); - let chunk = derive_chunk_context(42, "meander_body", ®ion, (0, 0)); - // Find a channel tile and a floodplain tile at the same along-axis position. - let mut channel_elev: Option = None; - let mut plain_elev: Option = None; - for tx in -200..200i32 { - let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, 50); - if col.water != Water::Dry && channel_elev.is_none() { - channel_elev = Some(col.elevation_m); - } - if col.water == Water::Dry { - plain_elev = Some(col.elevation_m); - } - if channel_elev.is_some() && plain_elev.is_some() { - break; + let chunk = anchor_chunk(42, "meander_body", ®ion); + let anchor = chunk.channel_anchor_m; + let mut min_wet: Option = None; + let mut max_dry: Option = None; + for c in anchor - 200..anchor + 200 { + let (tx, ty) = cross_along_to_tile(&chunk, c, 50); + let col = derive_voxel_column(42, "meander_body", ®ion, &chunk, tx, ty); + if col.water != Water::Dry { + min_wet = Some(min_wet.map_or(col.elevation_m, |e| e.min(col.elevation_m))); + } else { + max_dry = Some(max_dry.map_or(col.elevation_m, |e| e.max(col.elevation_m))); } } - if let (Some(ch_elev), Some(pl_elev)) = (channel_elev, plain_elev) { - assert!( - ch_elev <= pl_elev, - "MeanderReach: channel elevation ({ch_elev}) must be <= floodplain ({pl_elev})" - ); - } + // Both tile kinds must exist — a transect across the anchor that finds + // no channel means the placement regressed (the law must be exercised). + let (min_wet, max_dry) = ( + min_wet.expect("transect across the anchor must contain channel tiles"), + max_dry.expect("transect must contain floodplain tiles"), + ); + assert!( + min_wet <= max_dry, + "MeanderReach: channel bottom ({min_wet}) must be <= floodplain/levee top ({max_dry})" + ); } #[test] @@ -2984,31 +3011,41 @@ mod tests { morphology_zone: MorphologyZone::AlluvialPlain, ..shared_region_base }; - // Same seed + body → same ChunkContext (same wavelength, phase, channel_width). - let meander_chunk = derive_chunk_context(42, "sinuosity_body", &meander_reg, (0, 0)); - let alluvial_chunk = derive_chunk_context(42, "sinuosity_body", &alluvial_reg, (0, 0)); + // Same seed + body → same ChunkContext (same wavelength, phase, anchor, + // channel_width). Use the anchor-covering chunk (T-1040 band gating). + let meander_chunk = anchor_chunk(42, "sinuosity_body", &meander_reg); + let alluvial_chunk = anchor_chunk(42, "sinuosity_body", &alluvial_reg); // Verify the chunks share the same meander params (confirming the test setup). assert_eq!( meander_chunk.meander_phase, alluvial_chunk.meander_phase, "test setup requires identical chunk params" ); + assert_eq!( + meander_chunk.channel_anchor_m, alluvial_chunk.channel_anchor_m, + "test setup requires identical channel anchors" + ); assert!( meander_chunk.has_active_channel && alluvial_chunk.has_active_channel, "both chunks must have active channels for the test to be meaningful" ); - // Collect wet-tile x-positions over one full wavelength transect (at y=50). - // We scan far enough to capture the full meander swing — amplitude for - // MeanderReach is wl/4, for AlluvialPlain wl/6, so over a [−400,+400] sweep - // both complete multiple full cycles and their centreline positions diverge. - let meander_wet: Vec = (-400..400i32) - .filter(|&x| { - derive_voxel_column(42, "sinuosity_body", &meander_reg, &meander_chunk, x, 50).water + // Collect wet-tile cross-positions over a transect at along=50, centred + // on the shared anchor. We scan far enough to capture the full meander + // swing — amplitude for MeanderReach is wl/4, for AlluvialPlain wl/6, + // so over a ±400 sweep both complete multiple full cycles and their + // centreline positions diverge. + let anchor = meander_chunk.channel_anchor_m; + let meander_wet: Vec = (anchor - 400..anchor + 400) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&meander_chunk, c, 50); + derive_voxel_column(42, "sinuosity_body", &meander_reg, &meander_chunk, tx, ty) + .water != Water::Dry }) .collect(); - let alluvial_wet: Vec = (-400..400i32) - .filter(|&x| { - derive_voxel_column(42, "sinuosity_body", &alluvial_reg, &alluvial_chunk, x, 50) + let alluvial_wet: Vec = (anchor - 400..anchor + 400) + .filter(|&c| { + let (tx, ty) = cross_along_to_tile(&alluvial_chunk, c, 50); + derive_voxel_column(42, "sinuosity_body", &alluvial_reg, &alluvial_chunk, tx, ty) .water != Water::Dry }) diff --git a/server/tests/derivation_harness.rs b/server/tests/derivation_harness.rs index 46835c0b9..667632300 100644 --- a/server/tests/derivation_harness.rs +++ b/server/tests/derivation_harness.rs @@ -39,7 +39,7 @@ use std::path::PathBuf; use std::time::Instant; -use settled_reach_server::atlas::chunk_context::derive_chunk_context; +use settled_reach_server::atlas::chunk_context::{derive_chunk_context, BasinDirection, ChunkPos}; use settled_reach_server::atlas::drainage; use settled_reach_server::atlas::features::TerrainAnalysis; use settled_reach_server::atlas::heightmap::BodyHeightmap; @@ -100,6 +100,37 @@ fn derive_golden( } } +/// Compute the chunk position + tile coordinates sitting ON the region's +/// channel anchor at a given along-axis chunk index (T-1040/T-1041): feature +/// placement is region-anchored, so the golden pins a voxel on the anchor +/// column. Relocates automatically if the anchor derivation changes — which +/// flips the golden values anyway. +/// +/// `along_chunk` must stay within region (0, 0) — i.e. in `0..16` — so the +/// probed anchor belongs to the same region as the returned chunk. +fn anchor_golden_pos( + seed: u64, + body_id: &str, + region: &RegionProfile, + along_chunk: i32, +) -> ((i32, i32), i32, i32) { + assert!( + (0..16).contains(&along_chunk), + "along_chunk must stay within region (0, 0)" + ); + let probe = derive_chunk_context(seed, body_id, region, (0, 0)); + let anchor = probe.channel_anchor_m; + let along_tile = along_chunk * 64 + 32; + match probe.basin_direction { + BasinDirection::North | BasinDirection::South => { + ((anchor.div_euclid(64), along_chunk), anchor, along_tile) + } + BasinDirection::East | BasinDirection::West => { + ((along_chunk, anchor.div_euclid(64)), along_tile, anchor) + } + } +} + /// The three fixed golden inputs. Varied families and climate states. fn golden_cases() -> Vec<( &'static str, @@ -110,26 +141,45 @@ fn golden_cases() -> Vec<( i32, i32, )> { + // Cases A and C pin voxels on the region channel anchor (T-1040/T-1041): + // the channel/trough centreline is region-anchored, not at the world + // origin (A) or the chunk centre (C). + let alluvial_region = make_region( + MorphologyZone::AlluvialPlain, + TectonicClass::Active, + GlaciationGrade::None, + 6, + 22, + 18, + 68, + Some(12.0), + VegetationClass::Forest, + ); + let (alluvial_chunk_pos, alluvial_tx, alluvial_ty) = + anchor_golden_pos(0xdeadbeef_cafebabe_u64, "GJ144d", &alluvial_region, 12); + let fjord_region = make_region( + MorphologyZone::Fjord, + TectonicClass::Active, + GlaciationGrade::Moderate, + 55, + 60, + 28, + 55, + Some(-8.0), + VegetationClass::Barren, + ); + let (fjord_chunk_pos, fjord_tx, fjord_ty) = + anchor_golden_pos(0xfeedface_0badc0de_u64, "GJ447c", &fjord_region, 5); vec![ - // Case A: AlluvialPlain — temperate forest, active channel. + // Case A: AlluvialPlain — temperate forest, on the active-channel anchor. ( "alluvial_forest_active_channel", 0xdeadbeef_cafebabe_u64, "GJ144d", - make_region( - MorphologyZone::AlluvialPlain, - TectonicClass::Active, - GlaciationGrade::None, - 6, - 22, - 18, - 68, - Some(12.0), - VegetationClass::Forest, - ), - (10, 20), - 640, - 1280, + alluvial_region, + alluvial_chunk_pos, + alluvial_tx, + alluvial_ty, ), // Case B: LavaField — barren volcanic, no channel. ( @@ -151,26 +201,16 @@ fn golden_cases() -> Vec<( 320, 448, ), - // Case C: FjordWall — glaciated, rocky walls, deep water in trough. + // Case C: FjordWall — glaciated, rocky walls; tile on the region-anchored + // trough centreline (Deep water unless the warp nudges it onto the floor edge). ( "fjord_wall_glaciated", 0xfeedface_0badc0de_u64, "GJ447c", - make_region( - MorphologyZone::Fjord, - TectonicClass::Active, - GlaciationGrade::Moderate, - 55, - 60, - 28, - 55, - Some(-8.0), - VegetationClass::Barren, - ), - (14, 8), - // Centre of the chunk (fjord trough) — should be Deep water, Rock. - 14 * 64 + 32, - 8 * 64 + 32, + fjord_region, + fjord_chunk_pos, + fjord_tx, + fjord_ty, ), ] } @@ -320,8 +360,12 @@ fn law_drainage_monotonicity_alluvial_sweep() { VegetationClass::Forest, ); let mut checked = false; - for (cx, cy) in [(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)] { - checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, (cx, cy)); + // Sweep a spread of region chunks plus the channel-anchor band (T-1040: + // the channel is region-anchored, so only band chunks carry wet tiles). + let mut positions = vec![(0, 0), (1, 0), (0, 1), (4, 4), (8, 3)]; + positions.extend(anchor_band_chunks(42, "GJ144d", ®ion, (0, 0))); + for pos in positions { + checked |= assert_drainage_monotonicity(42, "GJ144d", "AlluvialPlain", ®ion, pos); } assert!( checked, @@ -344,8 +388,10 @@ fn law_drainage_monotonicity_meander_sweep() { VegetationClass::Forest, ); let mut checked = false; - for (cx, cy) in [(0, 0), (2, 1), (5, 5)] { - checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, (cx, cy)); + let mut positions = vec![(0, 0), (2, 1), (5, 5)]; + positions.extend(anchor_band_chunks(99, "GJ447c", ®ion, (0, 0))); + for pos in positions { + checked |= assert_drainage_monotonicity(99, "GJ447c", "MeanderReach", ®ion, pos); } assert!( checked, @@ -369,16 +415,27 @@ fn law_drainage_monotonicity_fjord_floor_at_sea_level() { Some(-8.0), VegetationClass::Barren, ); - let chunk = derive_chunk_context(42, "fjord_body", ®ion, (0, 0)); + // The fjord trough is region-anchored (T-1041): scan the chunk whose + // cross-range contains the channel anchor — only that chunk column carries + // the deep-water trough. + let probe = derive_chunk_context(42, "fjord_body", ®ion, (0, 0)); + let anchor_idx = probe.channel_anchor_m.div_euclid(64); + let chunk_pos = match probe.basin_direction { + BasinDirection::North | BasinDirection::South => (anchor_idx, 0), + BasinDirection::East | BasinDirection::West => (0, anchor_idx), + }; + let chunk = derive_chunk_context(42, "fjord_body", ®ion, chunk_pos); + let (base_x, base_y) = (chunk_pos.0 * 64, chunk_pos.1 * 64); let mut deep_elevs: Vec = vec![]; let mut dry_elevs: Vec = vec![]; // Scan the full 64×64 chunk, not a single row — the deep-water trough axis is - // not guaranteed to intersect any fixed Y, so a single-row probe could miss it - // entirely and silently pass without ever checking the sea-level claim. + // not guaranteed to intersect any fixed row, so a single-row probe could miss + // it entirely and silently pass without ever checking the sea-level claim. for dy in 0..64i32 { for dx in 0..64i32 { - let col = derive_voxel_column(42, "fjord_body", ®ion, &chunk, dx, dy); + let col = + derive_voxel_column(42, "fjord_body", ®ion, &chunk, base_x + dx, base_y + dy); match col.water { Water::Deep => deep_elevs.push(col.elevation_m), Water::Dry => dry_elevs.push(col.elevation_m), @@ -428,9 +485,12 @@ fn law_drainage_monotonicity_braided_delta() { VegetationClass::Scrub, ); let mut checked = false; - for (cx, cy) in [(0, 0), (1, 1)] { - checked |= - assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, (cx, cy)); + // The braid belt sits on the region's channel anchor (T-1041) — sweep the + // anchor band so the law is exercised on real thread tiles. + let mut positions = vec![(0, 0), (1, 1)]; + positions.extend(anchor_band_chunks(17, "delta_body", ®ion, (0, 0))); + for pos in positions { + checked |= assert_drainage_monotonicity(17, "delta_body", "BraidedDelta", ®ion, pos); } assert!( checked, @@ -898,6 +958,375 @@ fn law_climate_vegetation_airless_always_absent() { } } +// --------------------------------------------------------------------------- +// §2b — Region-anchored feature placement (T-1040 / T-1041, D-239 §10) +// --------------------------------------------------------------------------- +// +// T-1040: channel centrelines were anchored to the world x=0/y=0 axis — every +// chunk of a watered region claimed has_active_channel while channel voxels +// existed only near the world origin. T-1041: fjord/cliff/gorge/delta folded +// world coordinates into the 64 m chunk frame — region-scale landforms +// repeated every chunk. Both are fixed by region-anchored feature axes +// (`channel_anchor_m` / `coast_anchor_m`); these tests pin the placement +// contract far from the origin, at the ticket's example chunk (1000, −750). + +#[test] +fn channel_present_in_active_chunks_far_from_origin() { + // T-1040 (a): a has_active_channel chunk at an arbitrary large world + // offset contains in-channel voxels — and the gate is honest in both + // directions (wet ⇒ gated, ungated ⇒ dry). Sweeps the 16 cross-columns of + // the region containing chunk (1000, −750) at that chunk's along index. + let region = make_region( + MorphologyZone::AlluvialPlain, + TectonicClass::Stable, + GlaciationGrade::None, + 5, + 20, + 18, + 60, + Some(15.0), + VegetationClass::Forest, + ); + let (seed, body) = (42u64, "GJ144d"); + let ns = basin_is_ns(seed, body, ®ion, (1000, -750)); + let cross_base = if ns { + (1000 >> 4) << 4 + } else { + (-750i32 >> 4) << 4 + }; + let mut any_gated_wet = false; + let mut any_gate_off = false; + let mut total_wet = 0usize; + for i in 0..16 { + let pos: ChunkPos = if ns { + (cross_base + i, -750) + } else { + (1000, cross_base + i) + }; + let chunk = derive_chunk_context(seed, body, ®ion, pos); + let (bx, by) = (pos.0 * 64, pos.1 * 64); + let mut wet = 0usize; + for dy in 0..64i32 { + for dx in 0..64i32 { + let col = derive_voxel_column(seed, body, ®ion, &chunk, bx + dx, by + dy); + if col.water != Water::Dry { + wet += 1; + } + } + } + total_wet += wet; + if chunk.has_active_channel { + any_gated_wet |= wet > 0; + } else { + any_gate_off = true; + assert_eq!( + wet, 0, + "T-1040: chunk {pos:?} has no active channel but contains {wet} wet voxels \ + — the chunk gate and the voxel placement disagree" + ); + } + } + assert!( + total_wet > 0, + "T-1040: the region at chunk (1000, −750) must contain channel voxels \ + (pre-fix: zero — channels existed only near the world-origin axis)" + ); + assert!( + any_gated_wet, + "T-1040: at least one has_active_channel chunk must contain in-channel voxels" + ); + assert!( + any_gate_off, + "T-1040: the channel band must not blanket the region — some chunks must gate off" + ); +} + +#[test] +fn channel_continuous_across_chunk_boundary_far_from_origin() { + // T-1040 (a): channel position is continuous across adjacent chunk pairs. + // Every tile derives under its PRODUCTION covering chunk; the wet band's + // midpoint may not jump at a 64 m along-boundary (chunk-frame dependence + // would jump by up to a chunk width or drop out entirely). + let region = make_region( + MorphologyZone::AlluvialPlain, + TectonicClass::Stable, + GlaciationGrade::None, + 5, + 20, + 18, + 60, + Some(15.0), + VegetationClass::Forest, + ); + let (seed, body) = (42u64, "GJ144d"); + let probe = derive_chunk_context(seed, body, ®ion, (1000, -750)); + let ns = matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ); + let anchor = probe.channel_anchor_m; + // Wet-band midpoint of one world cross-row, sampling the full swept band. + let row_mid = |along: i32| -> Option { + let wet: Vec = (anchor - 220..anchor + 220) + .filter(|&c| { + derive_at_cross_along(seed, body, ®ion, ns, c, along).water != Water::Dry + }) + .collect(); + wet.first().map(|f| (f + wet.last().unwrap()) / 2) + }; + // An interior along-boundary of the region containing chunk (1000, −750): + // between along-chunks −744 and −743 (N/S) or 1004 and 1005 (E/W). + let boundary = if ns { -743 * 64 } else { 1005 * 64 }; + let mut prev: Option = None; + for along in boundary - 4..boundary + 4 { + let mid = row_mid(along).unwrap_or_else(|| { + panic!( + "T-1040: row along={along} contains no wet tiles — the channel \ + dropped out at the chunk boundary {boundary}" + ) + }); + if let Some(p) = prev { + assert!( + (mid - p).abs() <= 12, + "T-1040: wet-band midpoint jumped {} m between adjacent rows \ + {} and {} (boundary {boundary}) — channel is not continuous", + (mid - p).abs(), + along - 1, + along + ); + } + prev = Some(mid); + } +} + +#[test] +fn fjord_region_has_one_valley_spanning_chunks() { + // T-1041 (b): a FjordWall region contains ONE deep-water trough spanning + // its chunks — pre-fix a complete fjord cross-section repeated in every + // 64 m chunk. Representative sweep: two far regions × three along rows. + let region = make_region( + MorphologyZone::Fjord, + TectonicClass::Active, + GlaciationGrade::Moderate, + 55, + 60, + 28, + 55, + Some(-8.0), + VegetationClass::Barren, + ); + let (seed, body) = (42u64, "fjord_body"); + for region_chunk in [(640, -480), (-336, 992)] { + let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + let ns = matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ); + let (cross_chunk, along_chunk) = if ns { + (region_chunk.0, region_chunk.1) + } else { + (region_chunk.1, region_chunk.0) + }; + let cross_base = (cross_chunk >> 4) * 1024; + let along_base = (along_chunk >> 4) * 1024; + let positions: Vec = (cross_base..cross_base + 1024).collect(); + for along in [along_base + 32, along_base + 512, along_base + 992] { + let clusters = count_feature_clusters( + &positions, + |c| derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Deep, + 16, + ); + assert_eq!( + clusters, 1, + "T-1041: FjordWall region {region_chunk:?} must contain exactly ONE \ + deep-water trough across its 1024 m cross extent at along={along} \ + (got {clusters}; pre-fix: one per 64 m chunk)" + ); + } + } +} + +#[test] +fn gorge_region_has_one_valley_spanning_chunks() { + // T-1041 (b): an IncisedGorge region contains ONE shallow-floor gorge + // spanning its chunks — not one per chunk. + let region = make_region( + MorphologyZone::MountainPass, + TectonicClass::Active, + GlaciationGrade::None, + 50, + 65, + 5, + 40, + Some(5.0), + VegetationClass::Scrub, + ); + let (seed, body) = (42u64, "gorge_body"); + for region_chunk in [(640, -480), (-336, 992)] { + let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + let ns = matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ); + let (cross_chunk, along_chunk) = if ns { + (region_chunk.0, region_chunk.1) + } else { + (region_chunk.1, region_chunk.0) + }; + let cross_base = (cross_chunk >> 4) * 1024; + let along_base = (along_chunk >> 4) * 1024; + let positions: Vec = (cross_base..cross_base + 1024).collect(); + for along in [along_base + 32, along_base + 512, along_base + 992] { + let clusters = count_feature_clusters( + &positions, + |c| { + derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Shallow + }, + 16, + ); + assert_eq!( + clusters, 1, + "T-1041: IncisedGorge region {region_chunk:?} must contain exactly ONE \ + shallow gorge floor across its 1024 m cross extent at along={along} \ + (got {clusters}; pre-fix: one per 64 m chunk)" + ); + } + } +} + +#[test] +fn cliff_coast_one_continuous_coastline_per_region() { + // T-1041 (b): a CliffCoast region has ONE continuous (warp-displaced) + // coast line on the region-anchored face — pre-fix the cliff face sat at + // intra-chunk offset 48–55 in every chunk, sawtoothing the coast at 64 m + // pitch. Transect runs along the seaward (basin) axis across the region. + let region = make_region( + MorphologyZone::CliffCoast, + TectonicClass::Active, + GlaciationGrade::None, + 60, + 40, + 20, + 30, + Some(10.0), + VegetationClass::Scrub, + ); + let (seed, body) = (42u64, "cliff_body"); + for region_chunk in [(656, -464), (-256, 768)] { + let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + let coast = probe.coast_anchor_m; + let ns = matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ); + let (cross_chunk, along_chunk) = if ns { + (region_chunk.0, region_chunk.1) + } else { + (region_chunk.1, region_chunk.0) + }; + let along_base = (along_chunk >> 4) * 1024; + let cross_fixed = cross_chunk * 64 + 32; + let positions: Vec = (along_base..along_base + 1024).collect(); + // Exactly one ocean cluster (the seaward side of the one coast line). + let clusters = count_feature_clusters( + &positions, + |a| derive_at_cross_along(seed, body, ®ion, ns, cross_fixed, a).water == Water::Deep, + 16, + ); + assert_eq!( + clusters, 1, + "T-1041: CliffCoast region {region_chunk:?} must have exactly ONE ocean \ + side (got {clusters} Deep clusters; pre-fix: one 64 m sawtooth per chunk)" + ); + // Monotone coast: well inland of the face line → Dry; well seaward → + // Deep. Margins absorb the ±8 m domain warp (face 0..8, ledge 8..13). + for &a in &positions { + // Signed seaward distance — mirrors generate_cliff_coast. + let d = match probe.basin_direction { + BasinDirection::North | BasinDirection::West => coast - a, + BasinDirection::South | BasinDirection::East => a - coast, + }; + let col = derive_at_cross_along(seed, body, ®ion, ns, cross_fixed, a); + if d <= -9 { + assert_eq!( + col.water, + Water::Dry, + "T-1041: tile {} m inland of the coast line must be Dry (along={a})", + -d + ); + } else if d >= 21 { + assert_eq!( + col.water, + Water::Deep, + "T-1041: tile {d} m seaward of the coast line must be Deep (along={a})" + ); + } + } + } +} + +#[test] +fn braided_threads_confined_to_region_belt() { + // T-1041: braided threads anastomose across the region-anchored fan belt + // (anchor ± 32 m + thread width + warp) — pre-fix the same three threads + // restarted in every 64 m chunk, spreading thread water across the whole + // region's cross extent. + let region = make_region( + MorphologyZone::Delta, + TectonicClass::Active, + GlaciationGrade::None, + 3, + 8, + 25, + 50, + Some(18.0), + VegetationClass::Scrub, + ); + let (seed, body) = (17u64, "delta_body"); + for region_chunk in [(800, -592)] { + let probe = derive_chunk_context(seed, body, ®ion, region_chunk); + let ns = matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ); + let anchor = probe.channel_anchor_m; + let (cross_chunk, along_chunk) = if ns { + (region_chunk.0, region_chunk.1) + } else { + (region_chunk.1, region_chunk.0) + }; + let cross_base = (cross_chunk >> 4) * 1024; + let along = along_chunk * 64 + 32; + let wet: Vec = (cross_base..cross_base + 1024) + .filter(|&c| { + derive_at_cross_along(seed, body, ®ion, ns, c, along).water == Water::Shallow + }) + .collect(); + assert!( + !wet.is_empty(), + "T-1041: BraidedDelta region {region_chunk:?} must contain thread water" + ); + // Belt confinement: thread centres ∈ anchor ± 32, half-width ≤ 4, + // warp ≤ 8 → all thread water within anchor ± 44. + for &c in &wet { + assert!( + (c - anchor).abs() <= 44, + "T-1041: thread water at cross={c} is {} m from the fan axis {anchor} \ + — outside the region belt (pre-fix: threads repeated every chunk)", + (c - anchor).abs() + ); + } + // Braided, not single-thread: 1–3 thread clusters within the belt + // (three threads, possibly merged where centres overlap; warp-scale + // gap tolerance — the ±8 m warp punches small holes in a thread). + let cluster_count = count_feature_clusters(&wet, |_| true, 8); + assert!( + (1..=3).contains(&cluster_count), + "T-1041: expected 1–3 braided thread clusters in the belt, got {cluster_count}" + ); + } +} + // --------------------------------------------------------------------------- // §3 — Per-family <5 ms/chunk budget (D-239 §10) // --------------------------------------------------------------------------- @@ -1446,6 +1875,92 @@ fn validation_gruenfeld_skipped_not_in_wiki() { // Shared helpers // --------------------------------------------------------------------------- +/// Whether the region's basin runs north/south (cross axis = x). Region-scale +/// property — identical for every chunk of the region containing `region_chunk`. +fn basin_is_ns(seed: u64, body_id: &str, region: &RegionProfile, region_chunk: ChunkPos) -> bool { + let probe = derive_chunk_context(seed, body_id, region, region_chunk); + matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ) +} + +/// The chunk positions of the channel-anchor band (anchor column ± 1) across +/// the full along-extent of the region containing `region_chunk` (T-1040). +/// +/// The channel/landform centreline is region-anchored: it lives in the anchor +/// chunk column, swinging up to one meander amplitude sideways. The meander +/// wavelength (≤ ~650 m) fits inside the region's 1024 m along-extent, so the +/// centreline crosses the anchor column at least once — sweeping this band +/// guarantees wet tiles are exercised somewhere in it. +fn anchor_band_chunks( + seed: u64, + body_id: &str, + region: &RegionProfile, + region_chunk: ChunkPos, +) -> Vec { + let probe = derive_chunk_context(seed, body_id, region, region_chunk); + let anchor_idx = probe.channel_anchor_m.div_euclid(64); + let ns = matches!( + probe.basin_direction, + BasinDirection::North | BasinDirection::South + ); + // Region base index on the along axis (16-chunk regions; arithmetic shift + // = floor division, correct for negative chunks). + let along_base = if ns { + (region_chunk.1 >> 4) << 4 + } else { + (region_chunk.0 >> 4) << 4 + }; + let mut out = Vec::new(); + for cross in anchor_idx - 1..=anchor_idx + 1 { + for j in along_base..along_base + 16 { + out.push(if ns { (cross, j) } else { (j, cross) }); + } + } + out +} + +/// Derive one voxel at world cross/along coordinates under the PRODUCTION +/// covering chunk's context (per-tile chunk lookup — exactly what a consumer +/// streaming the world does). Basis-aware: cross ⊥ basin, along ∥ basin. +fn derive_at_cross_along( + seed: u64, + body_id: &str, + region: &RegionProfile, + ns: bool, + cross: i32, + along: i32, +) -> settled_reach_server::atlas::voxel::VoxelColumn { + let (tx, ty) = if ns { (cross, along) } else { (along, cross) }; + let chunk_pos = (tx.div_euclid(64), ty.div_euclid(64)); + let chunk = derive_chunk_context(seed, body_id, region, chunk_pos); + derive_voxel_column(seed, body_id, region, &chunk, tx, ty) +} + +/// Count clusters of positions where `pred` holds across a cross/along +/// transect, merging runs separated by gaps ≤ `gap_tolerance` (domain-warp +/// jitter can fragment a single feature by a few metres; distinct per-chunk +/// repeats are ≥ ~50 m apart and never merge). +fn count_feature_clusters( + positions: &[i32], + hits: impl Fn(i32) -> bool, + gap_tolerance: i32, +) -> usize { + let mut clusters = 0usize; + let mut last_hit: Option = None; + for &p in positions { + if hits(p) { + match last_hit { + Some(prev) if p - prev <= gap_tolerance => {} + _ => clusters += 1, + } + last_hit = Some(p); + } + } + clusters +} + /// Construct a `RegionProfile` directly from parameters, deriving the /// dependent fields (precipitation_class, river_threshold) consistently. fn make_region( diff --git a/server/tests/golden/derivation_harness.json b/server/tests/golden/derivation_harness.json index 97685400c..caa02cdd4 100644 --- a/server/tests/golden/derivation_harness.json +++ b/server/tests/golden/derivation_harness.json @@ -3,12 +3,12 @@ "label": "alluvial_forest_active_channel", "seed": 16045690984503098046, "body_id": "GJ144d", - "tile_x": 640, - "tile_y": 1280, + "tile_x": 223, + "tile_y": 800, "terrain": 0, - "vegetation": 1, - "water": 0, - "elevation_m": 11, + "vegetation": 4, + "water": 1, + "elevation_m": 8, "cover": 0 }, { @@ -27,12 +27,12 @@ "label": "fjord_wall_glaciated", "seed": 18369614217980264670, "body_id": "GJ447c", - "tile_x": 928, - "tile_y": 544, + "tile_x": 228, + "tile_y": 352, "terrain": 3, "vegetation": 0, "water": 2, - "elevation_m": 1, + "elevation_m": 0, "cover": 1 } ]