#include "Overlays.hpp" #include "Map2D.hpp" // projLonLat (2D projection of overlays) #include "Colors.hpp" // elevationColor (subgrid patch) #include "rlgl.h" #include "Projection.hpp" // dirToLonLat / lonLatToDir #include #include #include // std::abs(int) void buildBorders(const Planet& p, float radius, std::vector& real, std::vector& ridge) { real.clear(); ridge.clear(); auto midV = [&](int i, int j) -> Vector3 { Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius; return Vector3{ (float)m.x, (float)m.y, (float)m.z }; }; auto isBaby = [&](int pid){ return pid >= 0 && pid < (int)p.plates.size() && p.plates[pid].baby; }; auto emit = [&](const Vector3& a, const Vector3& b, int pA, int pB) { std::vector& out = (isBaby(pA) || isBaby(pB)) ? ridge : real; out.push_back(a); out.push_back(b); }; const std::vector& tri = p.triIndices(); for (size_t k = 0; k + 2 < tri.size(); k += 3) { int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2]; int pa = p.cells[ia].plateId, pb = p.cells[ib].plateId, pc = p.cells[ic].plateId; if (pa == pb && pb == pc) continue; if (pa != pb && pb != pc && pa != pc) { Vec3 c = ((p.cells[ia].unit + p.cells[ib].unit + p.cells[ic].unit) * (1.0 / 3.0)).normalized() * radius; Vector3 C{ (float)c.x, (float)c.y, (float)c.z }; emit(C, midV(ia, ib), pa, pb); emit(C, midV(ib, ic), pb, pc); emit(C, midV(ic, ia), pc, pa); } else { int lone, o1, o2; if (pa == pb) { lone = ic; o1 = ia; o2 = ib; } else if (pb == pc) { lone = ia; o1 = ib; o2 = ic; } else { lone = ib; o1 = ia; o2 = ic; } emit(midV(lone, o1), midV(lone, o2), p.cells[lone].plateId, p.cells[o1].plateId); } } } // Trace boundaries where a per-cell integer label differs across a triangle's cells (the plate // dual-contour, parameterised by the label array). Shared by nation + culture borders. static void buildLabelBorders(const Planet& p, float radius, const std::vector& cn, std::vector& segs) { segs.clear(); if ((int)cn.size() != (int)p.cells.size()) return; auto midV = [&](int i, int j) -> Vector3 { Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius; return Vector3{ (float)m.x, (float)m.y, (float)m.z }; }; auto emit = [&](const Vector3& a, const Vector3& b) { segs.push_back(a); segs.push_back(b); }; const std::vector& tri = p.triIndices(); for (size_t k = 0; k + 2 < tri.size(); k += 3) { int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2]; int na = cn[ia], nb = cn[ib], nc = cn[ic]; if (na == nb && nb == nc) continue; if (na != nb && nb != nc && na != nc) { Vec3 c = ((p.cells[ia].unit + p.cells[ib].unit + p.cells[ic].unit) * (1.0 / 3.0)).normalized() * radius; Vector3 C{ (float)c.x, (float)c.y, (float)c.z }; emit(C, midV(ia, ib)); emit(C, midV(ib, ic)); emit(C, midV(ic, ia)); } else { int lone, o1, o2; if (na == nb) { lone = ic; o1 = ia; o2 = ib; } else if (nb == nc) { lone = ia; o1 = ib; o2 = ic; } else { lone = ib; o1 = ia; o2 = ic; } emit(midV(lone, o1), midV(lone, o2)); } } } void buildNationBorders(const Planet& p, float radius, std::vector& segs) { buildLabelBorders(p, radius, p.cellNation(), segs); } void buildCultureBorders(const Planet& p, float radius, std::vector& segs) { buildLabelBorders(p, radius, p.cellCulture(), segs); } void buildWarFrontier(const Planet& p, float radius, std::vector& segs) { segs.clear(); const std::vector& cn = p.cellNation(); if (p.warList().empty() || (int)cn.size() != (int)p.cells.size()) return; // A short tick across each cell edge whose two realms are currently at war (the war front). for (int i = 0; i < (int)p.cells.size(); ++i) { int a = cn[i]; if (a < 0) continue; for (int j : p.cells[i].neighbors) { if (j <= i) continue; // each undirected edge once int b = (j < (int)cn.size()) ? cn[j] : -1; if (b < 0 || b == a || !p.realmsAtWar(a, b)) continue; Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius; Vec3 t = (p.cells[j].unit - p.cells[i].unit).normalized() * (radius * 0.03); segs.push_back(Vector3{ (float)(m.x - t.x), (float)(m.y - t.y), (float)(m.z - t.z) }); segs.push_back(Vector3{ (float)(m.x + t.x), (float)(m.y + t.y), (float)(m.z + t.z) }); } } } void buildDriftArrows(const Planet& p, float radius, std::vector& out, std::vector& labels) { out.clear(); labels.clear(); int np = (int)p.plates.size(); if (np == 0) return; std::vector sum(np, Vec3{0, 0, 0}); std::vector cnt(np, 0); for (const auto& c : p.cells) if (c.plateId >= 0 && c.plateId < np) { sum[c.plateId] = sum[c.plateId] + c.unit; ++cnt[c.plateId]; } std::vector centroid(np), vel(np); double vmax = 1e-30; for (int i = 0; i < np; ++i) { if (cnt[i] == 0) continue; centroid[i] = sum[i].normalized(); Vec3 omega = p.plates[i].driftAxis * p.plates[i].driftSpeed; vel[i] = omega.cross(centroid[i]); vmax = std::max(vmax, vel[i].length()); } const double shaftMin = 0.12, shaftMax = 0.45; auto V = [](const Vec3& v) { return Vector3{ (float)v.x, (float)v.y, (float)v.z }; }; for (int i = 0; i < np; ++i) { if (cnt[i] == 0 || vel[i].length() < 1e-12) continue; Vec3 dir = vel[i].normalized(); double len = shaftMin + (shaftMax - shaftMin) * (vel[i].length() / vmax); Vec3 base = centroid[i] * (double)radius; Vec3 tip = base + dir * len; Vec3 perp = dir.cross(centroid[i]).normalized(); double hl = len * 0.30; Vec3 back = dir * -1.0; Vec3 h1 = tip + (back * 0.8 + perp * 0.6) * hl; Vec3 h2 = tip + (back * 0.8 - perp * 0.6) * hl; out.push_back(V(base)); out.push_back(V(tip)); out.push_back(V(tip)); out.push_back(V(h1)); out.push_back(V(tip)); out.push_back(V(h2)); Vec3 lbl = centroid[i] * (radius + 0.015); // just above the planet surface at the plate centroid labels.push_back({i, V(lbl)}); } } void buildRivers(const Planet& p, float radius, std::vector& rivers, std::vector& bigRivers) { rivers.clear(); bigRivers.clear(); const std::vector& disc = p.discharge(); const std::vector& flow = p.flowTo(); if (disc.empty() || flow.empty()) return; const double thr = p.cfg.riverThreshold; auto V = [&](int i) { Vec3 m = p.cells[i].unit * (double)radius; return Vector3{ (float)m.x, (float)m.y, (float)m.z }; }; for (int i = 0; i < (int)p.cells.size(); ++i) { int d = flow[i]; if (d < 0 || disc[i] < thr) continue; // not a river / reached the sea (disc[i] > thr * 6.0 ? bigRivers : rivers).push_back(V(i)); (disc[i] > thr * 6.0 ? bigRivers : rivers).push_back(V(d)); } } void buildCoastline(const Planet& p, float radius, std::vector& segs, std::vector& oceanCell) { segs.clear(); oceanCell.clear(); const double sea = p.cfg.seaLevel; auto isLand = [&](int i) { return p.cells[i].elevation > sea; }; auto midV = [&](int i, int j) -> Vector3 { Vec3 m = ((p.cells[i].unit + p.cells[j].unit) * 0.5).normalized() * radius; return Vector3{ (float)m.x, (float)m.y, (float)m.z }; }; // The ocean cell nearest a coast segment, for sampling tide. Prefer an ocean endpoint of // the cut edge; fall back to the lone/other cell that is ocean. auto emit = [&](const Vector3& a, const Vector3& b, int oc) { segs.push_back(a); segs.push_back(b); oceanCell.push_back(oc); }; const std::vector& tri = p.triIndices(); for (size_t k = 0; k + 2 < tri.size(); k += 3) { int ia = tri[k], ib = tri[k + 1], ic = tri[k + 2]; bool la = isLand(ia), lb = isLand(ib), lc = isLand(ic); if (la == lb && lb == lc) continue; // all land or all ocean: no coast // Exactly one vertex differs from the other two -> one cut separating it. int lone, o1, o2; if (la == lb) { lone = ic; o1 = ia; o2 = ib; } else if (lb == lc) { lone = ia; o1 = ib; o2 = ic; } else { lone = ib; o1 = ia; o2 = ic; } int oc = isLand(lone) ? o1 : lone; // the ocean side of the cut emit(midV(lone, o1), midV(lone, o2), oc); } } void buildCurrents(const Planet& p, float radius, std::vector& segs, std::vector& cols) { segs.clear(); cols.clear(); const std::vector& cur = p.current(); if (cur.empty()) return; const double sea = p.cfg.seaLevel; double maxSp = 1e-9; for (const Vec3& v : cur) maxSp = std::max(maxSp, v.length()); const Vec3 up{0, 1, 0}; const Color warm{235, 120, 90, 255}, cold{90, 160, 235, 255}; auto V = [](const Vec3& q) { return Vector3{ (float)q.x, (float)q.y, (float)q.z }; }; for (int i = 0; i < (int)p.cells.size(); i += 3) { // subsample for readability if (p.cells[i].elevation > sea) continue; const Vec3& vel = cur[i]; double sp = vel.length(); if (sp < 0.18 * maxSp) continue; // skip the slack water const Vec3& nrm = p.cells[i].unit; Vec3 dir = vel * (1.0 / sp); double len = 0.02 + 0.03 * (sp / maxSp); Vec3 base = nrm * (double)radius; Vec3 tip = base + dir * len; Vec3 perp = dir.cross(nrm).normalized(); Vec3 back = dir * -1.0; double hl = len * 0.35; Vec3 h1 = tip + (back * 0.8 + perp * 0.6) * hl; Vec3 h2 = tip + (back * 0.8 - perp * 0.6) * hl; // Warm if flowing poleward (toward the nearer pole), cold if equatorward. Vec3 northT = up - nrm * up.dot(nrm); double nl = northT.length(); double pw = 0.0; if (nl > 1e-9) { northT = northT * (1.0 / nl); pw = dir.dot(northT) * (nrm.y >= 0 ? 1.0 : -1.0); } Color c = pw >= 0.0 ? warm : cold; segs.push_back(V(base)); segs.push_back(V(tip)); cols.push_back(c); segs.push_back(V(tip)); segs.push_back(V(h1)); cols.push_back(c); segs.push_back(V(tip)); segs.push_back(V(h2)); cols.push_back(c); } } std::vector> buildGraticule() { std::vector> g; const double D = M_PI / 180.0; for (int la = -60; la <= 60; la += 30) { // parallels std::vector pl; for (int lo = -180; lo <= 180; lo += 5) pl.push_back({ (float)(lo * D), (float)(la * D) }); g.push_back(pl); } for (int lo = -180; lo < 180; lo += 30) { // meridians std::vector pl; for (int la = -85; la <= 85; la += 5) pl.push_back({ (float)(lo * D), (float)(la * D) }); g.push_back(pl); } return g; } void drawGraticule3D(const std::vector>& g, float radius) { rlBegin(RL_LINES); rlColor4ub(110, 125, 150, 150); for (const auto& pl : g) for (size_t i = 0; i + 1 < pl.size(); ++i) { Vec3 a = lonLatToDir(pl[i].x, pl[i].y) * (double)radius; Vec3 b = lonLatToDir(pl[i + 1].x, pl[i + 1].y) * (double)radius; rlVertex3f((float)a.x, (float)a.y, (float)a.z); rlVertex3f((float)b.x, (float)b.y, (float)b.z); } rlEnd(); } void drawGraticule2D(const std::vector>& g, Rectangle r, double lonOffset) { rlBegin(RL_LINES); rlColor4ub(110, 125, 150, 150); for (const auto& pl : g) for (size_t i = 0; i + 1 < pl.size(); ++i) { Vector2 pa = projLonLat(pl[i].x, pl[i].y, lonOffset, r); Vector2 pb = projLonLat(pl[i + 1].x, pl[i + 1].y, lonOffset, r); if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue; rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y); } rlEnd(); } void drawGraticuleLabels2D(Rectangle r, double lonOffset) { const double D = M_PI / 180.0; const Color col{ 170, 185, 205, 220 }; // Latitudes at the left edge (Equal Earth y depends only on lat). const int lats[] = { 60, 30, 0, -30, -60 }; for (int la : lats) { Vector2 p = projLonLat(0.0, la * D, lonOffset, r); const char* t = (la == 0) ? "0" : TextFormat("%d%c", std::abs(la), la > 0 ? 'N' : 'S'); DrawText(t, (int)r.x + 3, (int)p.y - 6, 11, col); } // Longitudes along the bottom edge (skip any panned off the map). const int lons[] = { -180, -120, -60, 0, 60, 120, 180 }; int by = (int)(r.y + r.height) - 14; for (int lo : lons) { Vector2 p = projLonLat(lo * D, 0.0, lonOffset, r); if (p.x < r.x + 2 || p.x > r.x + r.width - 2) continue; const char* t = (lo == 0) ? "0" : TextFormat("%d%c", std::abs(lo), lo > 0 ? 'E' : 'W'); DrawText(t, (int)p.x - 8, by, 11, col); } } void drawSegments2D(const std::vector& segs, Color col, float width, Rectangle r, double lonOffset) { if (segs.empty()) return; rlSetLineWidth(width); rlBegin(RL_LINES); rlColor4ub(col.r, col.g, col.b, 255); for (size_t i = 0; i + 1 < segs.size(); i += 2) { Vec3 a = Vec3{segs[i].x, segs[i].y, segs[i].z}.normalized(); Vec3 b = Vec3{segs[i + 1].x, segs[i + 1].y, segs[i + 1].z}.normalized(); double alo, ala, blo, bla; dirToLonLat(a, alo, ala); dirToLonLat(b, blo, bla); Vector2 pa = projLonLat(alo, ala, lonOffset, r), pb = projLonLat(blo, bla, lonOffset, r); if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue; rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y); } rlEnd(); rlSetLineWidth(1.0f); } void drawColoredSegments2D(const std::vector& segs, const std::vector& cols, float width, Rectangle r, double lonOffset) { if (segs.empty()) return; rlSetLineWidth(width); rlBegin(RL_LINES); for (size_t i = 0, c = 0; i + 1 < segs.size(); i += 2, ++c) { Vec3 a = Vec3{segs[i].x, segs[i].y, segs[i].z}.normalized(); Vec3 b = Vec3{segs[i + 1].x, segs[i + 1].y, segs[i + 1].z}.normalized(); double alo, ala, blo, bla; dirToLonLat(a, alo, ala); dirToLonLat(b, blo, bla); Vector2 pa = projLonLat(alo, ala, lonOffset, r), pb = projLonLat(blo, bla, lonOffset, r); if (fabsf(pa.x - pb.x) > r.width * 0.5f) continue; const Color& col = cols[c < cols.size() ? c : cols.size() - 1]; rlColor4ub(col.r, col.g, col.b, 255); rlVertex2f(pa.x, pa.y); rlVertex2f(pb.x, pb.y); } rlEnd(); rlSetLineWidth(1.0f); } void drawSubgrids(const std::vector>& sgs, float visBase, float elevExagg, double seaLevel, float eps) { for (const auto& sg : sgs) { if (!sg || sg->res < 2) continue; int R = sg->res; rlBegin(RL_TRIANGLES); for (int j = 0; j < R - 1; ++j) for (int i = 0; i < R - 1; ++i) { const SubCell* q[4] = { &sg->sub[(size_t)j * R + i], &sg->sub[(size_t)j * R + i + 1], &sg->sub[(size_t)(j + 1) * R + i + 1], &sg->sub[(size_t)(j + 1) * R + i] }; const int order[6] = { 0, 1, 2, 0, 2, 3 }; for (int o : order) { const SubCell* s = q[o]; Color col = elevationColor(s->elevation, seaLevel); float rr = visBase + (float)s->elevation * elevExagg + eps; rlColor4ub(col.r, col.g, col.b, 255); rlVertex3f((float)(s->unit.x * rr), (float)(s->unit.y * rr), (float)(s->unit.z * rr)); } } rlEnd(); } }