Per terminal basin of DrainageAnalysis (reused, untouched): the spill cell/height on the render flow surface (boundary minimum of FullFilled, cross-checked bit-for-bit against BitDecrement(min inside)), lake-identity on classify (in-basin non-ocean classify water >= ISLA_LAKE_MIN_PX, 20000), and the downstream edge (the reference's FullFilled descent started at the spill, cross-checked against Plan.Dir). Seabed pits — terminal basins entirely under the classify sea — tagged and excluded from statistics. Per-lake ownership table. BasinGraphTool: chain → analysis → layer → CSVs → plate, height digests asserted around it. DrainageRenderer: five primitives private→internal. No height mutated, no water filled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EppUMXNhSeuA5Mu51UnTyP
151 lines
7.2 KiB
C#
151 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Godot;
|
|
using IslaApocalypse.Core;
|
|
|
|
namespace IslaApocalypse.Tools
|
|
{
|
|
/// <summary>
|
|
/// ⭐ THE BASIN-GRAPH PLATE (rivers/04) — the taste gate on the foundation. Presentation only; reads
|
|
/// the graph and the height, writes pixels. Nothing here touches data.
|
|
///
|
|
/// What the eye is meant to check, per the task:
|
|
/// • is each SPILL (yellow ring, labelled) at the true low rim where water would actually overtop?
|
|
/// • which basins hold REAL LAKES (blue tint, the lake cells brighter) vs DRY sinks (amber tint)?
|
|
/// • does "who drains to whom" (the arrows: cyan → ocean, white → another basin, red = closed)
|
|
/// look like a physically sane network?
|
|
///
|
|
/// Label at each spill: <c>"23M/7"</c> = spill 23 m above the sea datum, 7 m climb from the basin
|
|
/// floor (the number the cap is judged against). Both on the RENDER surface.
|
|
/// </summary>
|
|
public static class BasinGraphRenderer
|
|
{
|
|
private static readonly Color LakeTint = new(0.250f, 0.520f, 1.000f);
|
|
private static readonly Color LakeWater = new(0.180f, 0.420f, 0.980f);
|
|
private static readonly Color DryTint = new(0.980f, 0.660f, 0.250f);
|
|
private static readonly Color OutlineL = new(0.100f, 0.250f, 0.650f);
|
|
private static readonly Color OutlineD = new(0.600f, 0.330f, 0.060f);
|
|
private static readonly Color Spill = new(1.000f, 0.930f, 0.350f);
|
|
private static readonly Color EdgeOcean = new(0.250f, 0.900f, 1.000f);
|
|
private static readonly Color EdgeBasin = new(1.000f, 1.000f, 1.000f);
|
|
private static readonly Color EdgeNone = new(1.000f, 0.250f, 0.250f);
|
|
private static readonly Color Floor = new(0.050f, 0.050f, 0.050f);
|
|
private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
|
|
private static readonly Color Shadow = new(0.000f, 0.000f, 0.000f);
|
|
private static readonly Color SeabedOutline = new(0.180f, 0.300f, 0.520f);
|
|
|
|
public static Image Plate(BasinGraph g, DrainageAnalysis.Plan plan, Image img, int n,
|
|
bool[] isOcean, bool[] isClassifyWater, string title, string subtitle, string third)
|
|
{
|
|
int[] basinId = plan.BasinId;
|
|
int total = n * n;
|
|
|
|
// 1. tint every basin cell by lake/dry; lake cells inside a basin drawn as water.
|
|
var lakeOf = new Dictionary<int, bool>();
|
|
var seabed = new HashSet<int>();
|
|
foreach (var b in g.Nodes) { lakeOf[b.Id] = b.IsLake; if (b.IsSeabed) seabed.Add(b.Id); }
|
|
for (int i = 0; i < total; i++)
|
|
{
|
|
int id = basinId[i];
|
|
if (id == 0 || seabed.Contains(id)) continue;
|
|
int x = i / n, y = i % n;
|
|
bool isLakeBasin = lakeOf.TryGetValue(id, out bool l) && l;
|
|
if (isClassifyWater[i] && !isOcean[i])
|
|
{
|
|
img.SetPixel(x, y, isLakeBasin ? LakeWater : img.GetPixel(x, y).Lerp(LakeWater, 0.55f));
|
|
continue;
|
|
}
|
|
img.SetPixel(x, y, img.GetPixel(x, y).Lerp(isLakeBasin ? LakeTint : DryTint, 0.38f));
|
|
}
|
|
// 2. outline: a basin cell with a 4-neighbour of a different id.
|
|
for (int i = 0; i < total; i++)
|
|
{
|
|
int id = basinId[i];
|
|
if (id == 0) continue;
|
|
int x = i / n, y = i % n;
|
|
bool edge = (x > 0 && basinId[i - n] != id) || (x < n - 1 && basinId[i + n] != id)
|
|
|| (y > 0 && basinId[i - 1] != id) || (y < n - 1 && basinId[i + 1] != id);
|
|
if (!edge) continue;
|
|
if (seabed.Contains(id)) { img.SetPixel(x, y, SeabedOutline); continue; } // the seam: outline only
|
|
bool isLakeBasin = lakeOf.TryGetValue(id, out bool l) && l;
|
|
img.SetPixel(x, y, isLakeBasin ? OutlineL : OutlineD);
|
|
}
|
|
|
|
int thin = n >= 4096 ? 3 : 2, ring = n >= 4096 ? 16 : 9, ringW = n >= 4096 ? 4 : 3;
|
|
int floorR = n >= 4096 ? 6 : 3, head = n >= 4096 ? 22 : 12;
|
|
int scale = n >= 4096 ? 3 : 2;
|
|
|
|
// 3. the edges — the spill walk, arrowhead at the downstream end.
|
|
foreach (var b in g.LandNodes)
|
|
{
|
|
Color c = b.Downstream switch
|
|
{
|
|
DownstreamKind.Ocean => EdgeOcean,
|
|
DownstreamKind.Basin => EdgeBasin,
|
|
_ => EdgeNone,
|
|
};
|
|
var path = b.SpillPath;
|
|
if (path.Count >= 2)
|
|
{
|
|
for (int i = 1; i < path.Count; i++)
|
|
DrainageRenderer.Line(img, path[i - 1] / n, path[i - 1] % n, path[i] / n, path[i] % n, n, c, thin);
|
|
Arrowhead(img, path, n, c, head, thin);
|
|
}
|
|
else
|
|
{
|
|
// A spill that is itself the terminus (the rim cell is ocean) — or stuck on the spot.
|
|
DrainageRenderer.Disc(img, b.SpillCell / n, b.SpillCell % n, ring / 2, n, c);
|
|
}
|
|
}
|
|
// 4. spills, floors, labels.
|
|
foreach (var b in g.LandNodes)
|
|
{
|
|
int sx = b.SpillCell / n, sy = b.SpillCell % n;
|
|
DrainageRenderer.Ring(img, sx, sy, ring, n, Spill, ringW);
|
|
DrainageRenderer.Disc(img, b.FloorCell / n, b.FloorCell % n, floorR, n, Floor);
|
|
string lbl = $"{b.SpillAboveSeaM:F0}M/{b.SpillClimbM:F0}"; // TinyFont has no '+' or '^': "spill m above sea / climb m from floor"
|
|
Label(img, lbl, sx + ring + 4, sy - TinyFont.Height(scale) / 2, scale, n);
|
|
Label(img, $"#{b.Id}", b.FloorCell / n + floorR + 3, b.FloorCell % n - TinyFont.Height(scale) / 2, scale, n);
|
|
}
|
|
|
|
// 5. the legend.
|
|
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
|
|
TinyFont.Draw(img, title, 12, 12, s, Ink);
|
|
TinyFont.Draw(img, subtitle, 12, 12 + lh, s, Ink);
|
|
TinyFont.Draw(img, third, 12, 12 + lh * 2, s, Ink);
|
|
TinyFont.Draw(img, "BLUE TINT = LAKE BASIN (SIGNIFICANT CLASSIFY WATER INSIDE) AMBER TINT = DRY SINK BLACK DOT = BASIN FLOOR (#ID)", 12, 12 + lh * 3, s, Ink);
|
|
TinyFont.Draw(img, "YELLOW RING = SPILL CELL. LABEL 12M/4 = SPILL 12 M ABOVE SEA / 4 M CLIMB FROM THE BASIN FLOOR TO OVERTOP (RENDER SURFACE)", 12, 12 + lh * 4, s, Ink);
|
|
TinyFont.Draw(img, "ARROW = WHERE THE SPILL DRAINS: CYAN TO OCEAN, WHITE INTO ANOTHER BASIN, RED = CLOSED. DATA LAYER ONLY - NOTHING FILLED, NOTHING CARVED", 12, 12 + lh * 5, s, Ink);
|
|
TinyFont.Draw(img, "FAINT BLUE OUTLINE, NO MARKS = SEABED PIT (A RENDER DEPRESSION UNDER THE CLASSIFY SEA - THE D-046 SEAM, INERT, EXCLUDED FROM THE GRAPH)", 12, 12 + lh * 6, s, Ink);
|
|
return img;
|
|
}
|
|
|
|
private static void Arrowhead(Image img, List<int> path, int n, Color c, int len, int thick)
|
|
{
|
|
int end = path[^1];
|
|
int from = path[Math.Max(0, path.Count - 1 - 24)];
|
|
float ex = end / n, ey = end % n, fx = from / n, fy = from % n;
|
|
float dx = ex - fx, dy = ey - fy;
|
|
float L = MathF.Sqrt(dx * dx + dy * dy);
|
|
if (L < 1f) return;
|
|
dx /= L; dy /= L;
|
|
// two barbs, 30° either side of the reversed direction
|
|
const float a = 0.5236f;
|
|
float cs = MathF.Cos(a), sn = MathF.Sin(a);
|
|
float bx1 = -dx * cs - (-dy) * sn, by1 = -dx * sn + (-dy) * cs;
|
|
float bx2 = -dx * cs + (-dy) * sn, by2 = -(-dx) * sn + (-dy) * cs;
|
|
DrainageRenderer.Line(img, (int)ex, (int)ey, (int)(ex + bx1 * len), (int)(ey + by1 * len), n, c, thick);
|
|
DrainageRenderer.Line(img, (int)ex, (int)ey, (int)(ex + bx2 * len), (int)(ey + by2 * len), n, c, thick);
|
|
}
|
|
|
|
/// <summary>Ink over a one-px black shadow, clamped inside the image so a rim label near the edge is still readable.</summary>
|
|
private static void Label(Image img, string text, int x, int y, int scale, int n)
|
|
{
|
|
int w = TinyFont.Width(text, scale), h = TinyFont.Height(scale);
|
|
x = Math.Clamp(x, 0, Math.Max(0, n - w - 1));
|
|
y = Math.Clamp(y, 0, Math.Max(0, n - h - 1));
|
|
TinyFont.Draw(img, text, x + 1, y + 1, scale, Shadow);
|
|
TinyFont.Draw(img, text, x, y, scale, Ink);
|
|
}
|
|
}
|
|
}
|