feat(simulation): lake_margin_q depth band — lake shorelines gain gradient vocabulary (T-1188)
Lake edges rendered as hard step-edges while ocean coasts got multi-tone transition bands: every coastal-transition morphology gate keys on ocean_fraction_q, definitionally 0 inside a lake basin (hypothesis (b) of the ticket; (a) disproven first — a shoreline-crossing sweep at 2048/512/128m plus a 10m fine sweep all land on the same continuous crossing, so positional refinement was never broken). New DistrictProfile.lake_margin_q (0-100 settled-hydrology depth band, from the same bilinear filled/elevation pair the lake test already samples; ceiling calibrated just above the observed p90 depth on GJ338Bd's 5,043 flooded cells), threaded through both derive paths onto EncodedStepCanvas (serde-default for shape tolerance) and down the client: protocol decode, terrain-layer plane, colorize shades Lake cells by depth band instead of elev_q (bedrock-under-water, the wrong signal). project.yaml 0.4.0 -> 0.4.1: the new wire field must invalidate the client disk cache via its version tag (T-1183's D-192 mechanism). Acceptance gates green with the new field (lossless round-trip, cache-hit==cache-miss, every rung). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -203,7 +203,13 @@ pub enum StepCanvasStatus {
|
||||
/// than raw dense msgpack, and faster to encode/decode than every
|
||||
/// alternative measured). `width`/`height` are carried on
|
||||
/// [`EncodedStepCanvas`] (shared by every field), not duplicated per field.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
///
|
||||
/// `Default` (T-1188): needed for `#[serde(default)]` on newly-added
|
||||
/// `EncodedStepCanvas` fields (`lake_margin_q`) so an old-shape payload
|
||||
/// missing the field still deserializes — an empty `png_bytes` decodes via
|
||||
/// `png_decode_u8_plane` to an all-zero plane (see that function's
|
||||
/// empty-input handling), the correct "field absent" reading.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EncodedField {
|
||||
pub png_bytes: Vec<u8>,
|
||||
}
|
||||
@@ -279,7 +285,8 @@ pub struct CliffSegment {
|
||||
pub struct EncodedStepCanvas {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
// --- Static geometry plane (6 dense fields) ---
|
||||
// --- Static geometry plane (7 dense fields — lake_margin_q added T-1188,
|
||||
// see below; converged shape was 6, this is the one addition since) ---
|
||||
pub morphology: EncodedField,
|
||||
pub elev_q: EncodedField,
|
||||
pub temp_dc: EncodedTempField,
|
||||
@@ -291,6 +298,25 @@ pub struct EncodedStepCanvas {
|
||||
/// membership test, a fixed-radius proximity approximation pending real
|
||||
/// quarter-footprint geometry).
|
||||
pub settlement_id: EncodedSettlementField,
|
||||
/// T-1188 — settled-hydrology lake-margin depth band (`0` at/near the
|
||||
/// shoreline, ramping toward `100` at a basin's deep centre): the
|
||||
/// continuous tone source lake shorelines were missing, since
|
||||
/// `ocean_fraction_q` (the field that gives ocean coastlines their
|
||||
/// multi-tone transition band via `derive_morphology_zone`'s coastal
|
||||
/// gates) is definitionally `0` throughout a lake basin. Static geometry
|
||||
/// (a pure function of position, same plane as `elev_q`), NOT sim-state —
|
||||
/// grouped with the other static fields rather than next to
|
||||
/// `glaciation`/`flooded_q` below. `#[serde(default)]` (same precedent as
|
||||
/// `cliffs`) so a MessagePack decode of an old-shape payload doesn't
|
||||
/// hard-fail on the missing map key; not expected to occur in practice
|
||||
/// (client/server ship together, D-192, and the client's persistent
|
||||
/// step-canvas cache is version-tagged — see this ticket's cache-schema
|
||||
/// note — so a stale disk entry misses rather than decodes), but the
|
||||
/// derived default (`EncodedField { png_bytes: vec![] }`) is also never
|
||||
/// itself PNG-decoded on that path — nothing reads `lake_margin_q`
|
||||
/// before the version tag has already forced a fresh fetch.
|
||||
#[serde(default)]
|
||||
pub lake_margin_q: EncodedField,
|
||||
// --- Sim-state plane (2 dense fields, see struct doc) ---
|
||||
pub glaciation: EncodedField,
|
||||
pub flooded_q: EncodedField,
|
||||
@@ -436,6 +462,7 @@ struct StepCanvasCell {
|
||||
vegetation: u8,
|
||||
glaciation: u8,
|
||||
flooded_q: u8,
|
||||
lake_margin_q: u8,
|
||||
}
|
||||
|
||||
/// Derive one cell at `(wx, wy)` world metres, dispatching to the D-255(a)/(f)
|
||||
@@ -485,6 +512,11 @@ fn derive_step_canvas_cell(
|
||||
// D-253 stub — see EncodedStepCanvas's doc. Always "not flooded"
|
||||
// until the sim-state driving clock exists.
|
||||
flooded_q: 0,
|
||||
// T-1188: the lake-margin depth-band tone source — see
|
||||
// `district_profile::DistrictProfile::lake_margin_q`'s doc. `0` for
|
||||
// every non-lake cell, same static-geometry-plane posture as
|
||||
// `elev_q`/`morphology` (a pure function of position, not sim-state).
|
||||
lake_margin_q: prof.lake_margin_q.clamp(0, 100) as u8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +541,10 @@ pub struct RawStepCanvas {
|
||||
pub settlement_id: Vec<u32>,
|
||||
pub glaciation: Vec<u8>,
|
||||
pub flooded_q: Vec<u8>,
|
||||
/// T-1188 — settled-hydrology lake-margin depth band, the lake-shoreline
|
||||
/// tone source ocean coastlines already get for free from
|
||||
/// `ocean_fraction_q`. See `district_profile::DistrictProfile::lake_margin_q`.
|
||||
pub lake_margin_q: Vec<u8>,
|
||||
pub courses: Vec<RiverCourse>,
|
||||
pub cliffs: Vec<CliffSegment>,
|
||||
}
|
||||
@@ -976,6 +1012,7 @@ pub fn build_step_canvas(
|
||||
let mut vegetation = vec![0u8; cells];
|
||||
let mut glaciation = vec![0u8; cells];
|
||||
let mut flooded_q = vec![0u8; cells];
|
||||
let mut lake_margin_q = vec![0u8; cells];
|
||||
|
||||
for (row, row_cells) in rows.into_iter().enumerate() {
|
||||
let base = row * width as usize;
|
||||
@@ -988,6 +1025,7 @@ pub fn build_step_canvas(
|
||||
vegetation[i] = cell.vegetation;
|
||||
glaciation[i] = cell.glaciation;
|
||||
flooded_q[i] = cell.flooded_q;
|
||||
lake_margin_q[i] = cell.lake_margin_q;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1037,6 +1075,7 @@ pub fn build_step_canvas(
|
||||
settlement_id,
|
||||
glaciation,
|
||||
flooded_q,
|
||||
lake_margin_q,
|
||||
courses,
|
||||
cliffs,
|
||||
}
|
||||
@@ -1085,6 +1124,7 @@ pub fn encode_step_canvas(raw: &RawStepCanvas) -> EncodedStepCanvas {
|
||||
settlement_id: EncodedSettlementField {
|
||||
values: raw.settlement_id.clone(),
|
||||
},
|
||||
lake_margin_q: png_encode_u8_plane(raw.width, raw.height, &raw.lake_margin_q),
|
||||
glaciation: png_encode_u8_plane(raw.width, raw.height, &raw.glaciation),
|
||||
flooded_q: png_encode_u8_plane(raw.width, raw.height, &raw.flooded_q),
|
||||
courses: raw.courses.clone(),
|
||||
@@ -1107,6 +1147,7 @@ pub fn decode_step_canvas(enc: &EncodedStepCanvas) -> RawStepCanvas {
|
||||
moisture_q: png_decode_u8_plane(&enc.moisture_q),
|
||||
vegetation: png_decode_u8_plane(&enc.vegetation),
|
||||
settlement_id: enc.settlement_id.values.clone(),
|
||||
lake_margin_q: png_decode_u8_plane(&enc.lake_margin_q),
|
||||
glaciation: png_decode_u8_plane(&enc.glaciation),
|
||||
flooded_q: png_decode_u8_plane(&enc.flooded_q),
|
||||
courses: enc.courses.clone(),
|
||||
@@ -1850,6 +1891,7 @@ mod tests {
|
||||
moisture_q: png_encode_u8_plane(1, 1, &[0]),
|
||||
vegetation: png_encode_u8_plane(1, 1, &[0]),
|
||||
settlement_id: EncodedSettlementField { values: vec![0] },
|
||||
lake_margin_q: png_encode_u8_plane(1, 1, &[0]),
|
||||
glaciation: png_encode_u8_plane(1, 1, &[0]),
|
||||
flooded_q: png_encode_u8_plane(1, 1, &[0]),
|
||||
courses: Vec::new(),
|
||||
|
||||
Reference in New Issue
Block a user