feat(client): invert the Atlas rung relation — rung sets extent, not spacing
A rung used to fix the gridunit SPACING, with the canvas extent falling out of spacing x cell count. That is why the top of the ladder was unusable: at REGION_M spacing a viewport-sized canvas spanned ~251,658 km — six times around a rocky body — so the Region rung capped to the body and redrew the Global picture pixel-for-pixel. "Global and region look the same" was not a rendering bug; it was this relation, stated in metres. Inverted: a rung fixes the EXTENT and the spacing falls out of the canvas size. The shorter viewport axis spans exactly one cell of the rung's level, so a widescreen window shows more ground on the long axis rather than less on the short one. Every rung now shows the ground its name promises — Region 262x466 km, District 4.1x7.3 km — and the canvas cell count is viewport-driven and identical at every rung, so derive cost no longer varies with depth and resize is free. Consequences that fell out of the inversion rather than being chosen: - Region leaves the orbital derive set. It was envelope-only because at 251,658 km nothing finer made sense; at 262 km it is a genuine provincial map and takes the full courses-aware derive. Region having no rivers at all was much of why the top of the ladder read flat. It also joins the deep display ratio for the same reason. - The S2 station-spacing floor is deleted, not retuned. It guarded an O(1/spacing) blowup that the inversion makes structurally impossible (the canvas cell count is now constant across rungs, so stations-per-course is bounded however deep you scroll). Kept, it would do active harm in the opposite direction: a 2,048 m pitch across a 3.6 km District canvas places two stations and draws every river as a straight line. Station placement gets its own generator pass. - cap_extent_to_body is superseded and now a documented no-op. A canvas can no longer over-request a body by construction. The residual question — whether a rung's cell exceeds the whole body — is liveness, not capping, and is_rung_live_on_body() answers it by omitting the rung. Empirically it never fires on inhabited content: all six rungs are live on all 271 populated bodies with a radius. - snap_to_gridunit no longer truncates its multiplier to int. Post-inversion the deep rungs run sub-metre (Chunk ~0.12 m at a 1080 px short axis), where int(spacing) floors to zero and would collapse every request centre onto the origin. Both sides derive spacing from the same three inputs (rung, echoed cell extent, body radius) rather than one telling the other, so there is nothing to keep in sync beyond the constant table itself. Body radius already reaches the viewer via enter(); no wire change. Pair session with Jeroen, 2026-07-26. D-243/D-255 amendments to be backfiled. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+207
-126
@@ -91,23 +91,58 @@ pub enum StepCanvasRung {
|
||||
}
|
||||
|
||||
impl StepCanvasRung {
|
||||
/// Cell spacing in metres for every FIXED rung — sourced from `scale::`
|
||||
/// (D-243), never a magic number (mirrors `WindowGranularity::spacing_m`'s
|
||||
/// discipline). [`Self::Global`] has no single spacing value (its
|
||||
/// gridunit is "one region", not a metre float) — callers needing
|
||||
/// Global's cell pitch use [`Self::global_cell_counts`] instead; this
|
||||
/// method still returns `REGION_M` for `Global` as a harmless-but-correct
|
||||
/// value (a Global gridunit and a Region gridunit are both "one region"
|
||||
/// wide) so ordering/comparison call sites that don't special-case
|
||||
/// `Global` still get a sane, documented number rather than 0 or a panic.
|
||||
pub fn spacing_m(self) -> f64 {
|
||||
/// The rung's own world-metre **cell size** — sourced from `scale::`
|
||||
/// (D-243), never a magic number. `None` for [`Self::Global`], the sole
|
||||
/// elastic rung, whose extent is the body itself and therefore cannot be
|
||||
/// a constant (D-243's elastic seam).
|
||||
///
|
||||
/// **This is an extent, not a spacing** (D-255 amendment, pair session
|
||||
/// 2026-07-26). The original ladder had it the other way round: a rung
|
||||
/// fixed the gridunit *spacing* and the canvas extent fell out of
|
||||
/// `spacing × cell count`. That inversion is what made the top of the
|
||||
/// ladder unusable — at `REGION_M` spacing a viewport-sized canvas spanned
|
||||
/// ~251,658 km, six times around a rocky body, so the Region rung capped
|
||||
/// to the body and redrew the Global picture pixel-for-pixel. Now the rung
|
||||
/// fixes the extent and the spacing falls out of the canvas size
|
||||
/// ([`Self::spacing_m`]), so every rung shows exactly the ground its name
|
||||
/// promises and the scroll walks the D-243 stair honestly.
|
||||
pub fn extent_m(self) -> Option<f64> {
|
||||
match self {
|
||||
StepCanvasRung::Global => scale::REGION_M as f64,
|
||||
StepCanvasRung::Region => scale::REGION_M as f64,
|
||||
StepCanvasRung::District => scale::DISTRICT_M as f64,
|
||||
StepCanvasRung::Quarter => scale::QUARTER_M as f64,
|
||||
StepCanvasRung::Block => scale::BLOCK_M as f64,
|
||||
StepCanvasRung::Chunk => scale::CHUNK_M as f64,
|
||||
StepCanvasRung::Global => None,
|
||||
StepCanvasRung::Region => Some(scale::REGION_M as f64),
|
||||
StepCanvasRung::District => Some(scale::DISTRICT_M as f64),
|
||||
StepCanvasRung::Quarter => Some(scale::QUARTER_M as f64),
|
||||
StepCanvasRung::Block => Some(scale::BLOCK_M as f64),
|
||||
StepCanvasRung::Chunk => Some(scale::CHUNK_M as f64),
|
||||
}
|
||||
}
|
||||
|
||||
/// Metres per gridunit for a canvas of `width × height` cells on a body of
|
||||
/// `body_radius_km` — a function of the **request**, not a per-rung
|
||||
/// constant (D-255 amendment 2026-07-26, see [`Self::extent_m`]).
|
||||
///
|
||||
/// Fixed rungs: the **shorter** canvas axis spans exactly one cell of this
|
||||
/// level, so a widescreen viewport shows proportionally more ground on the
|
||||
/// long axis rather than less on the short one (Jeroen's rule: "the
|
||||
/// smallest viewport axis locks the area for calculation, since that is
|
||||
/// most widescreen friendly"). A consequence worth knowing: the canvas
|
||||
/// cell count is viewport-driven and identical at every rung, so derive
|
||||
/// cost no longer varies with depth.
|
||||
///
|
||||
/// [`Self::Global`]: equirectangular whole body — the full `2πR`
|
||||
/// circumference wraps the canvas **width**, and the 2:1 cell counts
|
||||
/// [`Self::global_cell_counts`] produces keep the cells square (the height
|
||||
/// spans `πR`, pole to pole). Callers pass the RESOLVED canvas dimensions
|
||||
/// ([`resolve_canvas_extent`]), never the requested ones — a clamped
|
||||
/// canvas has a coarser spacing over the same ground, and the derive must
|
||||
/// use what it actually got.
|
||||
pub fn spacing_m(self, width: u32, height: u32, body_radius_km: f64) -> f64 {
|
||||
match self.extent_m() {
|
||||
Some(extent_m) => extent_m / width.min(height).max(1) as f64,
|
||||
None => {
|
||||
let circumference_m = 2.0 * std::f64::consts::PI * body_radius_km * 1_000.0;
|
||||
circumference_m / width.max(1) as f64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,75 +393,17 @@ pub struct StepCanvasResponse {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Station-spacing cap for courses (S2 addendum decision — see module doc)
|
||||
// (Station-spacing cap removed — D-255 extent inversion, pair session
|
||||
// 2026-07-26. `COURSE_STATION_SPACING_FLOOR_M` / `course_station_spacing_m`
|
||||
// floored course resampling at DISTRICT_M to guard an O(1/spacing) blowup that
|
||||
// the inversion makes structurally impossible: the canvas cell count is now
|
||||
// viewport-driven and identical at every rung, so stations-per-course is
|
||||
// bounded at ~one per gridunit however deep the rung. Post-inversion the floor
|
||||
// would do active harm — a District canvas spans ~3.6 km, so a 2,048 m pitch
|
||||
// put two stations across the whole view and drew every river as a straight
|
||||
// line. Station placement gets its own generator pass (Jeroen, same session).)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Station-spacing floor for river-course invention at step-canvas rungs
|
||||
/// (T-1178/T-1154 S2 addendum measurement): `near_perennial_water`'s cost is
|
||||
/// `O(courses × points-per-course)`, and `invent_course` resamples each
|
||||
/// course's control polyline at the RUNG's own spacing — so a course gets
|
||||
/// proportionally MORE points the finer the rung, independent of whether
|
||||
/// that extra density serves the "is this cell near a river" riparian test
|
||||
/// at all. Measured cost: **+84.7% to +87.4% at Chunk (64 m stations,
|
||||
/// 1,732 pts/course), +37.6% to +51.8% at Block (128 m stations, 867
|
||||
/// pts/course)** — both a real, structural cost increase (not the <5%
|
||||
/// District-spacing figure).
|
||||
///
|
||||
/// **DECISION (T-1181 implementation, against the S2 numbers): ADOPT the
|
||||
/// cap.** Rationale:
|
||||
/// - The riparian test only needs "is this cell within the riparian band of
|
||||
/// a course," never full display-fidelity course geometry — station count
|
||||
/// beyond what the fixed riparian-band width already resolves is pure
|
||||
/// waste for that purpose.
|
||||
/// - +85-87% at Chunk is a real cost more than doubling `near_perennial_
|
||||
/// water`'s share of the per-cell budget at the ladder's deepest,
|
||||
/// most-frequently-panned rung — exactly where the response needs to
|
||||
/// stay snappy (D-255(a): Chunk is the rung "closest to the player,"
|
||||
/// 1×1 fidelity, the display band Stig's ⑥ measurement prioritizes).
|
||||
/// Paying it for zero riparian-accuracy benefit is not a tradeoff worth
|
||||
/// taking when a cap is a one-line `.max()` with no behavior change to
|
||||
/// what the client actually sees (courses still draw at full Stage-B
|
||||
/// fidelity in `RiverCourse.points` — the cap only floors the STATION
|
||||
/// RESAMPLING spacing used internally by `near_perennial_water`'s cost
|
||||
/// driver, not the wire polyline itself... **correction, see below.**)
|
||||
///
|
||||
/// **Where the cap is actually applied — sparse feature invention, not the
|
||||
/// per-cell riparian test.** Reading `invent_course`'s signature
|
||||
/// (`river_course.rs`): `station_spacing_m` is a SINGLE parameter that
|
||||
/// drives BOTH the wire polyline's resample density (Ruling 3b: "Stage B
|
||||
/// places stations at this spacing along global arc-length") AND the
|
||||
/// riparian-test cost (more stations = more `near_perennial_water` distance
|
||||
/// checks per course). There is no separate "riparian-only" spacing knob in
|
||||
/// the current `river_course` API — capping the ONE spacing value the
|
||||
/// step-canvas call site passes therefore caps both together, which is the
|
||||
/// right shape for the mandatory acceptance gate below: courses are content
|
||||
/// of the wire payload (Tyre round-2 §(a) "one flat tagged response"), so a
|
||||
/// deliberately coarser polyline at Chunk/Block is a real, visible display
|
||||
/// choice, not a hidden internal optimization — documented here as exactly
|
||||
/// that.
|
||||
///
|
||||
/// **The floor value: [`scale::DISTRICT_M`] (2,048 m).** District is the
|
||||
/// rung where T-1178/T-1154's OWN measurement found courses cost <5%
|
||||
/// (negligible, not staggered — Cross-check 1: "195.0 ns/cell — within 2% of
|
||||
/// the synthetic fixture's 192.0 ns/cell"). Never resampling finer than
|
||||
/// District's own station spacing means every fixed sub-District rung
|
||||
/// (Quarter/Block/Chunk) inherits that same negligible-cost band instead of
|
||||
/// paying the inverse-spacing S2 penalty, while District and Region (both
|
||||
/// already ≥ this floor) are completely unaffected — `.max(DISTRICT_M)` is a
|
||||
/// no-op for them by construction. Courses still refine in POSITION/shape
|
||||
/// per rung (a different edge set intersects a Chunk-sized window than a
|
||||
/// District-sized one — the cull is unaffected), only the per-course
|
||||
/// point-DENSITY stops increasing below District's own spacing.
|
||||
pub const COURSE_STATION_SPACING_FLOOR_M: f64 = scale::DISTRICT_M as f64;
|
||||
|
||||
/// Apply the [`COURSE_STATION_SPACING_FLOOR_M`] cap to a rung's own spacing —
|
||||
/// the single call site every step-canvas course-invention path routes
|
||||
/// through (mirrors `layer_proxy::window_world_rect`'s "shared by both
|
||||
/// consumers so it can never drift" discipline).
|
||||
pub fn course_station_spacing_m(rung: StepCanvasRung) -> f64 {
|
||||
rung.spacing_m().max(COURSE_STATION_SPACING_FLOOR_M)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quantization (mirrors layer_proxy::quantize_min_wl_m's discipline)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -667,12 +644,24 @@ fn invent_courses_for_canvas(
|
||||
river_network: &RiverNetwork,
|
||||
canvas_rect: (f64, f64, f64, f64),
|
||||
rung: StepCanvasRung,
|
||||
step_m: f64,
|
||||
min_wavelength_m: f64,
|
||||
) -> Vec<InventedCourse> {
|
||||
if rung.uses_orbital_derive() {
|
||||
return Vec::new();
|
||||
}
|
||||
let station_spacing_m = course_station_spacing_m(rung);
|
||||
// One station per gridunit — the finest density this canvas can draw.
|
||||
//
|
||||
// The former absolute floor ([`COURSE_STATION_SPACING_FLOOR_M`], 2,048 m)
|
||||
// guarded an O(1/spacing) blowup that the D-255 extent inversion (pair
|
||||
// session 2026-07-26) made structurally impossible: the canvas cell count
|
||||
// is now viewport-driven and IDENTICAL at every rung, so a course crossing
|
||||
// it has at most ~one station per gridunit no matter how deep the rung.
|
||||
// Keeping the absolute floor would now do active harm in the opposite
|
||||
// direction — a District canvas spans ~3.6 km post-inversion, so a 2,048 m
|
||||
// station pitch would place two stations across the whole view and render
|
||||
// every river as a straight line.
|
||||
let station_spacing_m = step_m;
|
||||
let (win_x0, win_y0, win_x1, win_y1) = canvas_rect;
|
||||
|
||||
let edges = river_course::build_edges(river_network);
|
||||
@@ -938,7 +927,9 @@ pub fn build_step_canvas(
|
||||
let body_radius_km = params.body_radius_km.unwrap_or(0.0);
|
||||
let (width, height) = resolve_canvas_extent(rung, extent, body_radius_km);
|
||||
let min_wavelength_m = min_wl_m as f64;
|
||||
let step_m = rung.spacing_m();
|
||||
// RESOLVED dims, not the requested `extent` — a clamped canvas covers the
|
||||
// same ground at a coarser pitch (see StepCanvasRung::spacing_m).
|
||||
let step_m = rung.spacing_m(width, height, body_radius_km);
|
||||
let cells = (width * height) as usize;
|
||||
|
||||
let half_w = (width / 2) as i32;
|
||||
@@ -972,6 +963,7 @@ pub fn build_step_canvas(
|
||||
river_network,
|
||||
canvas_rect,
|
||||
rung,
|
||||
step_m,
|
||||
min_wavelength_m,
|
||||
)
|
||||
};
|
||||
@@ -1640,16 +1632,95 @@ mod tests {
|
||||
// Rung vocabulary
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// A fixed rung's EXTENT is the D-243 constant (D-255 amendment, pair
|
||||
/// session 2026-07-26) — the inversion moved the constant from the
|
||||
/// spacing side of the relation to the extent side. Global has no
|
||||
/// constant extent at all: it is the elastic seam.
|
||||
#[test]
|
||||
fn spacing_m_matches_d243_constants() {
|
||||
assert_eq!(StepCanvasRung::Region.spacing_m(), scale::REGION_M as f64);
|
||||
fn extent_m_matches_d243_constants() {
|
||||
assert_eq!(
|
||||
StepCanvasRung::District.spacing_m(),
|
||||
scale::DISTRICT_M as f64
|
||||
StepCanvasRung::Region.extent_m(),
|
||||
Some(scale::REGION_M as f64)
|
||||
);
|
||||
assert_eq!(StepCanvasRung::Quarter.spacing_m(), scale::QUARTER_M as f64);
|
||||
assert_eq!(StepCanvasRung::Block.spacing_m(), scale::BLOCK_M as f64);
|
||||
assert_eq!(StepCanvasRung::Chunk.spacing_m(), scale::CHUNK_M as f64);
|
||||
assert_eq!(
|
||||
StepCanvasRung::District.extent_m(),
|
||||
Some(scale::DISTRICT_M as f64)
|
||||
);
|
||||
assert_eq!(
|
||||
StepCanvasRung::Quarter.extent_m(),
|
||||
Some(scale::QUARTER_M as f64)
|
||||
);
|
||||
assert_eq!(StepCanvasRung::Block.extent_m(), Some(scale::BLOCK_M as f64));
|
||||
assert_eq!(StepCanvasRung::Chunk.extent_m(), Some(scale::CHUNK_M as f64));
|
||||
assert_eq!(StepCanvasRung::Global.extent_m(), None);
|
||||
}
|
||||
|
||||
/// The inversion's core contract: the SHORTER canvas axis spans exactly
|
||||
/// one cell of the rung's level, whatever the viewport shape, so a
|
||||
/// widescreen window shows more ground on the long axis rather than less
|
||||
/// on the short one.
|
||||
#[test]
|
||||
fn shorter_axis_spans_exactly_one_rung_cell() {
|
||||
for (rung, cell_m) in [
|
||||
(StepCanvasRung::Region, scale::REGION_M as f64),
|
||||
(StepCanvasRung::District, scale::DISTRICT_M as f64),
|
||||
(StepCanvasRung::Quarter, scale::QUARTER_M as f64),
|
||||
(StepCanvasRung::Block, scale::BLOCK_M as f64),
|
||||
(StepCanvasRung::Chunk, scale::CHUNK_M as f64),
|
||||
] {
|
||||
// Landscape, portrait and square canvases must all put one whole
|
||||
// cell across the SHORT axis — the axis is chosen by size, never
|
||||
// by which one happens to be the width.
|
||||
for (w, h) in [(960u32, 540u32), (540, 960), (700, 700)] {
|
||||
let spacing = rung.spacing_m(w, h, 6_238.4);
|
||||
let short = w.min(h) as f64;
|
||||
assert!(
|
||||
(spacing * short - cell_m).abs() < 1e-9,
|
||||
"{rung:?} at {w}x{h}: short axis spans {} m, want {cell_m} m",
|
||||
spacing * short
|
||||
);
|
||||
// ...and the long axis therefore shows proportionally more.
|
||||
let long = w.max(h) as f64;
|
||||
assert!(spacing * long >= cell_m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spacing follows the canvas, not the rung: halving the cell count over
|
||||
/// the same rung doubles the pitch (a clamped canvas covers the same
|
||||
/// ground more coarsely — it does not cover less ground).
|
||||
#[test]
|
||||
fn spacing_scales_inversely_with_cell_count() {
|
||||
let fine = StepCanvasRung::District.spacing_m(960, 540, 6_238.4);
|
||||
let coarse = StepCanvasRung::District.spacing_m(480, 270, 6_238.4);
|
||||
assert!((coarse - fine * 2.0).abs() < 1e-9, "{coarse} vs {fine}");
|
||||
}
|
||||
|
||||
/// Global is the one rung whose spacing comes from the body: the full
|
||||
/// 2πR circumference wraps the canvas WIDTH (equirectangular), so a
|
||||
/// bigger body at the same cell count yields a proportionally coarser
|
||||
/// gridunit. This is D-243's elastic seam, and the only place a body
|
||||
/// radius enters the ladder at all.
|
||||
#[test]
|
||||
fn global_spacing_is_circumference_over_width() {
|
||||
let r_km = 6_238.4_f64;
|
||||
let spacing = StepCanvasRung::Global.spacing_m(960, 480, r_km);
|
||||
let circumference_m = 2.0 * std::f64::consts::PI * r_km * 1_000.0;
|
||||
assert!((spacing * 960.0 - circumference_m).abs() < 1e-6);
|
||||
// Twice the body, twice the pitch at the same cell count.
|
||||
let double = StepCanvasRung::Global.spacing_m(960, 480, r_km * 2.0);
|
||||
assert!((double - spacing * 2.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// Degenerate canvases must not divide by zero — a zero axis clamps to
|
||||
/// one cell rather than producing an infinity that would poison every
|
||||
/// world-metre computation downstream.
|
||||
#[test]
|
||||
fn zero_extent_does_not_divide_by_zero() {
|
||||
for rung in [StepCanvasRung::Global, StepCanvasRung::Chunk] {
|
||||
let spacing = rung.spacing_m(0, 0, 6_238.4);
|
||||
assert!(spacing.is_finite(), "{rung:?} produced {spacing}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1836,40 +1907,46 @@ mod tests {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Station-spacing cap (S2 addendum decision)
|
||||
// Station spacing — the S2 absolute floor is retired (see the note at
|
||||
// the top of this module where the constant used to live)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// The S2 station-spacing floor existed because course resampling ran at
|
||||
/// the rung's own spacing, so a course picked up proportionally MORE
|
||||
/// stations the deeper the rung — measured at +85% cost at Chunk. The
|
||||
/// extent inversion removes the mechanism rather than capping it: station
|
||||
/// pitch is now the canvas pitch, and the canvas cell count is
|
||||
/// viewport-driven and identical at every rung, so a course crossing the
|
||||
/// canvas gets the SAME station budget however deep you scroll.
|
||||
///
|
||||
/// This test guards that property directly — if a future change reties
|
||||
/// station density to absolute metres, the deep rungs will diverge here
|
||||
/// and this fails before the cost regression ships.
|
||||
#[test]
|
||||
fn station_spacing_cap_floors_fine_rungs_to_district() {
|
||||
// Chunk (64 m) and Block (128 m) are both finer than the District
|
||||
// floor — capped up to it.
|
||||
assert_eq!(
|
||||
course_station_spacing_m(StepCanvasRung::Chunk),
|
||||
COURSE_STATION_SPACING_FLOOR_M
|
||||
);
|
||||
assert_eq!(
|
||||
course_station_spacing_m(StepCanvasRung::Block),
|
||||
COURSE_STATION_SPACING_FLOOR_M
|
||||
);
|
||||
assert_eq!(
|
||||
course_station_spacing_m(StepCanvasRung::Quarter),
|
||||
COURSE_STATION_SPACING_FLOOR_M
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn station_spacing_cap_is_a_noop_at_and_above_district() {
|
||||
// District sits exactly at the floor — unaffected.
|
||||
assert_eq!(
|
||||
course_station_spacing_m(StepCanvasRung::District),
|
||||
scale::DISTRICT_M as f64
|
||||
);
|
||||
// Region is coarser than District — unaffected (also moot, since
|
||||
// Region never invents courses at all — uses_orbital_derive()).
|
||||
assert_eq!(
|
||||
course_station_spacing_m(StepCanvasRung::Region),
|
||||
scale::REGION_M as f64
|
||||
);
|
||||
fn station_budget_is_rung_independent() {
|
||||
let (w, h) = (960u32, 540u32);
|
||||
let budget = |rung: StepCanvasRung| {
|
||||
let spacing = rung.spacing_m(w, h, 6_238.4);
|
||||
// Stations a course spanning the canvas's long axis would take.
|
||||
(rung.extent_m().unwrap() * (w as f64 / h as f64)) / spacing
|
||||
};
|
||||
let district = budget(StepCanvasRung::District);
|
||||
for rung in [
|
||||
StepCanvasRung::Region,
|
||||
StepCanvasRung::Quarter,
|
||||
StepCanvasRung::Block,
|
||||
StepCanvasRung::Chunk,
|
||||
] {
|
||||
let got = budget(rung);
|
||||
assert!(
|
||||
(got - district).abs() < 1e-6,
|
||||
"{rung:?} station budget {got} diverges from District's {district} — \
|
||||
station density must not scale with rung depth"
|
||||
);
|
||||
}
|
||||
// And that shared budget is the canvas width, not an absolute metre
|
||||
// figure: ~one station per gridunit across the long axis.
|
||||
assert!((district - w as f64).abs() < 1e-6, "{district} vs {w}");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
@@ -2088,7 +2165,11 @@ mod tests {
|
||||
// centre on each axis — the corner cells are ~960 m from centre,
|
||||
// inside the radius; use a coarser rung to guarantee an
|
||||
// outside-radius cell exists).
|
||||
let step_m = StepCanvasRung::Block.spacing_m(); // 128 m/cell
|
||||
// A fixture pitch chosen for the geometry above, no longer read off a
|
||||
// rung: post-inversion a rung's spacing depends on the canvas size, so
|
||||
// `Block.spacing_m(24, 18, ..)` would be ~7 m and put every cell inside
|
||||
// the coverage radius, quietly destroying what this test checks.
|
||||
let step_m = 128.0; // m/cell
|
||||
let (width, height) = (24u32, 18u32);
|
||||
let half_w = (width / 2) as i32;
|
||||
let half_h = (height / 2) as i32;
|
||||
@@ -2148,7 +2229,7 @@ mod tests {
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
StepCanvasRung::Chunk.spacing_m(),
|
||||
64.0, // fixture pitch — see settlement_ids_for_canvas's own test above
|
||||
);
|
||||
let (centre_row, centre_col) = (1usize, 1usize);
|
||||
let centre_i = centre_row * 3 + centre_col;
|
||||
@@ -2167,7 +2248,7 @@ mod tests {
|
||||
2,
|
||||
4,
|
||||
4,
|
||||
StepCanvasRung::Chunk.spacing_m(),
|
||||
64.0, // fixture pitch — see settlement_ids_for_canvas's own test above
|
||||
);
|
||||
assert!(ids.iter().all(|&v| v == 0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user