Add high-resolution 2D map export (F11 / toolbar button)
Exports the full Equal Earth map as a ~4K PNG via an off-screen RenderTexture2D, reusing the existing Map2D/Overlays drawing code (already parameterized by an arbitrary target Rectangle) at scale. Extracted the overlay-drawing body of renderMap2D() into a shared, scale-parameterized Viewer::drawMapOverlays() so on-screen and export rendering can never diverge. Fixed a real bug found while testing: the toolbar button called exportMapImage() synchronously from inside drawToolbar()'s active scissor mode (for its scroll panel), clipping the export to a tiny corner. Now deferred via a pending flag and run right after drawToolbar() returns.
This commit is contained in:
parent
56d74aba7b
commit
ddeba0dea8
1
BUILD.md
1
BUILD.md
@ -72,6 +72,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
+ / - subdivision level (detail), 1..7
|
+ / - subdivision level (detail), 1..7
|
||||||
F5 / F9 save / load full state (planet.save)
|
F5 / F9 save / load full state (planet.save)
|
||||||
F12 screenshot to screenshot.png
|
F12 screenshot to screenshot.png
|
||||||
|
F11 export the 2D map as a high-resolution (~4K) PNG (also a toolbar button)
|
||||||
F2 reload planet.cfg (validated) and regenerate
|
F2 reload planet.cfg (validated) and regenerate
|
||||||
F1 toggle the clickable toolbar menu (top-right of the 3D globe; a button/
|
F1 toggle the clickable toolbar menu (top-right of the 3D globe; a button/
|
||||||
checkbox/dropdown for every key above, built with raygui)
|
checkbox/dropdown for every key above, built with raygui)
|
||||||
|
|||||||
28
CLAUDE.md
28
CLAUDE.md
@ -335,6 +335,31 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
|||||||
full-row-width checkbox call just drew an oversized glyph with the label clipped off-panel; fixed by
|
full-row-width checkbox call just drew an oversized glyph with the label clipped off-panel; fixed by
|
||||||
a small `guiCheckRow()` helper (`Toolbar.cpp`) that passes a compact glyph rect and draws the label
|
a small `guiCheckRow()` helper (`Toolbar.cpp`) that passes a compact glyph rect and draws the label
|
||||||
itself at a known position. Purely `src/render` — no save-format or `src/sim` change.
|
itself at a known position. Purely `src/render` — no save-format or `src/sim` change.
|
||||||
|
- **High-resolution map export** *(done — see `Viewer::exportMapImage()`, `src/render/ViewerRender.cpp`)*
|
||||||
|
— key **`F11`** (also a toolbar "Export Map" button, `World / Time` section) renders the full
|
||||||
|
(unzoomed) 2D Equal Earth map into an off-screen `RenderTexture2D` at **~4K** (3840 px wide, height
|
||||||
|
from `mapRect`'s current aspect ratio) and saves it as a timestamped `map_export_YYYY-MM-DD_HHMMSS.png`
|
||||||
|
— a "poster" export independent of the on-screen window size/zoom. Keeps the current colour mode and
|
||||||
|
longitude pan and respects every overlay toggle (borders/rivers/drift/tides/currents/clouds/volcanoes/
|
||||||
|
settlements/names — "exports what you're looking at, just bigger"), except minor place-name labels
|
||||||
|
(peaks/rivers/lakes/seas/small islands) are always shown regardless of on-screen zoom, since a 4K
|
||||||
|
canvas has the room. Enabled by a refactor: the ~90-line overlay-drawing body of `renderMap2D()` was
|
||||||
|
extracted into a shared `Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float
|
||||||
|
markerZoom, bool showMinorLabels)` (line widths × `scale`, font sizes × `scale`, marker radii ×
|
||||||
|
`markerZoom`) — the on-screen path passes `scale=1`/`markerZoom=min(2,mapZoom)` (pixel-identical to
|
||||||
|
before), the export path passes the export/on-screen width ratio for both, so the two callers can
|
||||||
|
never diverge. This works because every `Map2D`/`Overlays` draw function was already parameterized by
|
||||||
|
an arbitrary target `Rectangle`, not the window size — the existing `map2D` member (built at on-screen
|
||||||
|
resolution) is reused as-is for the export, since only its per-cell `lon`/`lat` are read (screen
|
||||||
|
position is recomputed per-rect by `drawMapTris`/`projLonLat`, so it isn't tied to the rect it was
|
||||||
|
built with). One real bug found + fixed while building this: the toolbar button initially called
|
||||||
|
`exportMapImage()` **synchronously** from inside its `GuiButton` click check — but `drawToolbar()`
|
||||||
|
wraps its whole scrollable content in a `BeginScissorMode`/`EndScissorMode` pair (for the scroll-panel
|
||||||
|
clip), so the export's render-texture pass inherited the toolbar's small on-screen clip rect and
|
||||||
|
produced an almost-empty image. Fixed by deferring: the button just sets `Viewer::pendingMapExport`,
|
||||||
|
and `renderFrame()` calls `exportMapImage()` right after `drawToolbar()` returns (scissor mode
|
||||||
|
guaranteed closed by then) — the `F11` key path was never affected (input handling runs before
|
||||||
|
`BeginDrawing()`, outside any scissor state). Purely `src/render` — no save-format or `src/sim` change.
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -877,7 +902,8 @@ settlement — with per-field lock against automatic recompute; see below) ·
|
|||||||
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
|
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
|
||||||
button/checkbox/dropdown there — see below) ·
|
button/checkbox/dropdown there — see below) ·
|
||||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
||||||
`F12` screenshot (`screenshot.png`) · `F2` reload `planet.cfg` + regenerate.
|
`F12` screenshot (`screenshot.png`) · `F11` export the 2D map as a high-resolution (~4K)
|
||||||
|
`map_export_<timestamp>.png` (also a toolbar button) · `F2` reload `planet.cfg` + regenerate.
|
||||||
|
|
||||||
Phase 3: after `phase3AfterMy` simulated years a modal prompt asks **Continue
|
Phase 3: after `phase3AfterMy` simulated years a modal prompt asks **Continue
|
||||||
Phase 2** / **Start Phase 3**; `H` starts/stops it manually. In Phase 3 drift
|
Phase 2** / **Start Phase 3**; `H` starts/stops it manually. In Phase 3 drift
|
||||||
|
|||||||
@ -185,6 +185,8 @@ void drawToolbar(Viewer& v) {
|
|||||||
if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Save (F5)")) v.saveGame(v.SAVE_PATH);
|
if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Save (F5)")) v.saveGame(v.SAVE_PATH);
|
||||||
if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) v.loadGame(v.SAVE_PATH);
|
if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) v.loadGame(v.SAVE_PATH);
|
||||||
}
|
}
|
||||||
|
// Deferred to after drawToolbar() returns -- see Viewer::pendingMapExport.
|
||||||
|
if (GuiButton(takeRow(cx, cw, cy), "Export Map (F11)")) v.pendingMapExport = true;
|
||||||
cy += SECTION_GAP;
|
cy += SECTION_GAP;
|
||||||
|
|
||||||
// --- Civilization & Ecology (all need a settled world) ------------------
|
// --- Civilization & Ecology (all need a settled world) ------------------
|
||||||
|
|||||||
@ -152,6 +152,11 @@ struct Viewer {
|
|||||||
Vector2 toolbarScroll{0, 0}; // GuiScrollPanel scroll offset
|
Vector2 toolbarScroll{0, 0}; // GuiScrollPanel scroll offset
|
||||||
int toolbarModeActive = 0; // GuiDropdownBox: selected view-mode index
|
int toolbarModeActive = 0; // GuiDropdownBox: selected view-mode index
|
||||||
bool toolbarModeEditMode = false; // GuiDropdownBox: is the list currently open
|
bool toolbarModeEditMode = false; // GuiDropdownBox: is the list currently open
|
||||||
|
bool pendingMapExport = false; // Export-Map button: deferred until after drawToolbar()
|
||||||
|
// returns, so its own BeginScissorMode/EndScissorMode
|
||||||
|
// (around the scroll content) isn't still active --
|
||||||
|
// that clip rect would otherwise also clip the export
|
||||||
|
// render-texture pass.
|
||||||
|
|
||||||
// Transient on-screen status line.
|
// Transient on-screen status line.
|
||||||
std::string statusMsg; double statusUntil = 0.0;
|
std::string statusMsg; double statusUntil = 0.0;
|
||||||
@ -199,6 +204,11 @@ struct Viewer {
|
|||||||
void liveStepBack(); // step everything back one frame (restores weather/storms)
|
void liveStepBack(); // step everything back one frame (restores weather/storms)
|
||||||
void wxPushSnapshot(); // push the current weather state onto the step-back ring
|
void wxPushSnapshot(); // push the current weather state onto the step-back ring
|
||||||
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
|
Rectangle mapViewRect() const; // 2D map projection rect after zoom/pan (scissor stays mapRect)
|
||||||
|
// Shared 2D-map overlay draw (borders/rivers/markers/labels): used by both the on-screen
|
||||||
|
// renderMap2D() (scale=1, markerZoom=on-screen zoom clamp) and exportMapImage() (both = the
|
||||||
|
// export resolution multiplier, so lines/fonts/markers scale up together at high-res).
|
||||||
|
void drawMapOverlays(Rectangle vr, double lonOffset, float scale, float markerZoom, bool showMinorLabels);
|
||||||
|
void exportMapImage(); // render the 2D map at high resolution and save as PNG (F11)
|
||||||
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
void appendEvent(uint8_t kind, uint8_t severity, double timeHours, int cell, uint32_t sourceId,
|
||||||
const std::string& title, const std::string& detail);
|
const std::string& title, const std::string& detail);
|
||||||
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
void detectLiveEvents(const std::vector<WeatherSystem>& beforeStorms,
|
||||||
|
|||||||
@ -242,6 +242,7 @@ void Viewer::handleInput() {
|
|||||||
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
||||||
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
||||||
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
||||||
|
if (IsKeyPressed(KEY_F11)) exportMapImage();
|
||||||
// Speed control: Live World ramps the live clock (sim hours/s, ~hour -> month);
|
// Speed control: Live World ramps the live clock (sim hours/s, ~hour -> month);
|
||||||
// otherwise it sets the drift rate (My simulated per real second).
|
// otherwise it sets the drift rate (My simulated per real second).
|
||||||
if (IsKeyPressed(KEY_RIGHT_BRACKET)) {
|
if (IsKeyPressed(KEY_RIGHT_BRACKET)) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "Projection.hpp" // dirToLonLat (plate labels)
|
#include "Projection.hpp" // dirToLonLat (plate labels)
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <ctime>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// Tint a settlement marker by its live environmental condition (1 = thriving, <1 = hardship/drought):
|
// Tint a settlement marker by its live environmental condition (1 = thriving, <1 = hardship/drought):
|
||||||
@ -380,35 +381,36 @@ void Viewer::renderGlobe3D() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2D Equal Earth map + its overlays (borders/drift/rivers/labels/markers).
|
// 2D Equal Earth map + its overlays (borders/drift/rivers/labels/markers).
|
||||||
void Viewer::renderMap2D() {
|
// Draws every 2D-map overlay (borders, rivers, drift arrows, weather, storm/volcano/settlement
|
||||||
DrawRectangleRec(mapRect, Color{6, 8, 14, 255});
|
// markers, plate + place-name labels) into rect `vr`. `scale` multiplies line widths and font
|
||||||
Rectangle vr = mapViewRect(); // projection rect (zoom/pan); scissor stays mapRect
|
// sizes (1 on-screen, the export resolution ratio for exportMapImage()); `markerZoom` multiplies
|
||||||
BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height);
|
// marker radii (on-screen: the existing zoom clamp; export: the same as `scale`, since there is no
|
||||||
drawMap2D(planet, displayColors(), map2D, vr, mapLon);
|
// on-screen "zoom" concept for a full-world export). Shared so the two callers can never diverge.
|
||||||
if (showGrat) { drawGraticule2D(graticule, vr, mapLon); drawGraticuleLabels2D(vr, mapLon); }
|
void Viewer::drawMapOverlays(Rectangle vr, double lonOffset, float scale, float markerZoom, bool showMinorLabels) {
|
||||||
if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f, vr, mapLon);
|
if (showGrat) { drawGraticule2D(graticule, vr, lonOffset); drawGraticuleLabels2D(vr, lonOffset); }
|
||||||
if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f, vr, mapLon);
|
if (showBorders && !borders.empty()) drawSegments2D(borders, Color{255, 235, 90, 255}, 2.0f * scale, vr, lonOffset);
|
||||||
if (showNationBorders && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f, vr, mapLon);
|
if (showBorders && !ridgeBorders.empty()) drawSegments2D(ridgeBorders, Color{220, 70, 60, 255}, 2.0f * scale, vr, lonOffset);
|
||||||
if (showCultureBorders && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f, vr, mapLon);
|
if (showNationBorders && !nationBorders.empty()) drawSegments2D(nationBorders, Color{18, 18, 26, 235}, 2.0f * scale, vr, lonOffset);
|
||||||
if (showNationBorders && !warFrontier.empty()) drawSegments2D(warFrontier, Color{235, 40, 30, 255}, 2.5f, vr, mapLon);
|
if (showCultureBorders && !cultureBorders.empty()) drawSegments2D(cultureBorders, Color{245, 240, 220, 230}, 2.5f * scale, vr, lonOffset);
|
||||||
if (showNationBorders && !allyLinks.empty()) drawSegments2D(allyLinks, Color{70, 220, 120, 220}, 1.5f, vr, mapLon);
|
if (showNationBorders && !warFrontier.empty()) drawSegments2D(warFrontier, Color{235, 40, 30, 255}, 2.5f * scale, vr, lonOffset);
|
||||||
if (showNationBorders && !rivalLinks.empty()) drawSegments2D(rivalLinks, Color{150, 40, 60, 220}, 1.5f, vr, mapLon);
|
if (showNationBorders && !allyLinks.empty()) drawSegments2D(allyLinks, Color{70, 220, 120, 220}, 1.5f * scale, vr, lonOffset);
|
||||||
if (showTradeRoutes && !tradeSea.empty()) drawSegments2D(tradeSea, Color{90, 200, 235, 220}, 1.2f, vr, mapLon);
|
if (showNationBorders && !rivalLinks.empty()) drawSegments2D(rivalLinks, Color{150, 40, 60, 220}, 1.5f * scale, vr, lonOffset);
|
||||||
if (showTradeRoutes && !tradeLand.empty()) drawSegments2D(tradeLand, Color{230, 180, 90, 220}, 1.2f, vr, mapLon);
|
if (showTradeRoutes && !tradeSea.empty()) drawSegments2D(tradeSea, Color{90, 200, 235, 220}, 1.2f * scale, vr, lonOffset);
|
||||||
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f, vr, mapLon);
|
if (showTradeRoutes && !tradeLand.empty()) drawSegments2D(tradeLand, Color{230, 180, 90, 220}, 1.2f * scale, vr, lonOffset);
|
||||||
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f, vr, mapLon);
|
if (showDrift && !driftArrows.empty()) drawSegments2D(driftArrows, Color{90, 230, 255, 255}, 2.0f * scale, vr, lonOffset);
|
||||||
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f, vr, mapLon);
|
if (liveWorld && showTides && !coastCols.empty()) drawColoredSegments2D(coast, coastCols, 2.0f * scale, vr, lonOffset);
|
||||||
if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, vr, mapLon);
|
if (showCurrents && !currentCols.empty()) drawColoredSegments2D(currentSegs, currentCols, 1.6f * scale, vr, lonOffset);
|
||||||
|
if (liveWorld && showClouds && !planet.cloud().empty()) drawWeather2D(planet, planet.cloud(), planet.rain(), map2D, vr, lonOffset);
|
||||||
if (liveWorld && showClouds && !planet.storms().empty()) {
|
if (liveWorld && showClouds && !planet.storms().empty()) {
|
||||||
for (const auto& ws : planet.storms()) {
|
for (const auto& ws : planet.storms()) {
|
||||||
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
double lon, lat; dirToLonLat(Vec3{ws.pos.x, ws.pos.y, ws.pos.z}, lon, lat);
|
||||||
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 sp = projLonLat(lon, lat, lonOffset, vr);
|
||||||
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
bool hur = ws.tropical && ws.strength >= planet.cfg.weatherHurricaneStr;
|
||||||
Color c = hur ? Color{240, 60, 60, 255} : Color{150, 200, 235, 255};
|
Color c = hur ? Color{240, 60, 60, 255} : Color{150, 200, 235, 255};
|
||||||
float rad = (5.0f + 10.0f * (float)ws.strength) * (float)std::min(2.0, mapZoom);
|
float rad = (5.0f + 10.0f * (float)ws.strength) * markerZoom;
|
||||||
DrawCircleLines((int)sp.x, (int)sp.y, rad, c);
|
DrawCircleLines((int)sp.x, (int)sp.y, rad, c);
|
||||||
if (hur) DrawCircleLines((int)sp.x, (int)sp.y, rad * 0.55f, c);
|
if (hur) DrawCircleLines((int)sp.x, (int)sp.y, rad * 0.55f, c);
|
||||||
DrawCircleV(sp, 2.0f, c);
|
DrawCircleV(sp, 2.0f * scale, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
if (liveWorld && showVolcanoes && !planet.volcanoes.empty()) {
|
||||||
@ -416,66 +418,76 @@ void Viewer::renderMap2D() {
|
|||||||
if (vc.cell < 0 || vc.cell >= (int)planet.cells.size()) continue;
|
if (vc.cell < 0 || vc.cell >= (int)planet.cells.size()) continue;
|
||||||
double er = planet.volcanoErupting(vc);
|
double er = planet.volcanoErupting(vc);
|
||||||
double lon, lat; dirToLonLat(planet.cells[vc.cell].unit, lon, lat);
|
double lon, lat; dirToLonLat(planet.cells[vc.cell].unit, lon, lat);
|
||||||
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 sp = projLonLat(lon, lat, lonOffset, vr);
|
||||||
float s = (5.0f + 3.0f * (float)er) * (float)std::min(2.0, mapZoom);
|
float s = (5.0f + 3.0f * (float)er) * markerZoom;
|
||||||
Color tri = vc.phase == 1 ? Color{125, 120, 115, 255}
|
Color tri = vc.phase == 1 ? Color{125, 120, 115, 255}
|
||||||
: vc.ashTimer > 0.0 ? Color{245, 170, 55, 255}
|
: vc.ashTimer > 0.0 ? Color{245, 170, 55, 255}
|
||||||
: Color{170, 75, 50, 255};
|
: Color{170, 75, 50, 255};
|
||||||
DrawPoly(sp, 3, s, -90.0f, tri); // filled up-pointing triangle (cone)
|
DrawPoly(sp, 3, s, -90.0f, tri); // filled up-pointing triangle (cone)
|
||||||
if (er > 0.12 && vc.phase != 1)
|
if (er > 0.12 && vc.phase != 1)
|
||||||
DrawCircleLines((int)sp.x, (int)sp.y, s + 3.0f,
|
DrawCircleLines((int)sp.x, (int)sp.y, s + 3.0f * scale,
|
||||||
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
Color{255, 170, 70, (unsigned char)std::clamp(90.0 + 150.0 * er, 0.0, 255.0)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showSettlements && !planet.settlements.empty()) {
|
if (showSettlements && !planet.settlements.empty()) {
|
||||||
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
const double townP = planet.cfg.civTownPop, cityP = planet.cfg.civCityPop, abP = planet.cfg.civAbandonPop;
|
||||||
const auto& cond = planet.settlementCondition();
|
const auto& cond = planet.settlementCondition();
|
||||||
float zf = (float)std::min(2.0, mapZoom);
|
|
||||||
for (size_t k = 0; k < planet.settlements.size(); ++k) {
|
for (size_t k = 0; k < planet.settlements.size(); ++k) {
|
||||||
const Settlement& s = planet.settlements[k];
|
const Settlement& s = planet.settlements[k];
|
||||||
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
if (s.cell < 0 || s.cell >= (int)planet.cells.size()) continue;
|
||||||
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
|
double lon, lat; dirToLonLat(planet.cells[s.cell].unit, lon, lat);
|
||||||
Vector2 sp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 sp = projLonLat(lon, lat, lonOffset, vr);
|
||||||
bool alive = s.population >= abP;
|
bool alive = s.population >= abP;
|
||||||
SettleTier t = settleTierOf(s.population, townP, cityP);
|
SettleTier t = settleTierOf(s.population, townP, cityP);
|
||||||
float rad = (t == SettleTier::City ? 4.5f : t == SettleTier::Town ? 3.2f : 2.2f) * zf;
|
float rad = (t == SettleTier::City ? 4.5f : t == SettleTier::Town ? 3.2f : 2.2f) * markerZoom;
|
||||||
Color col = !alive ? Color{120, 120, 126, 255}
|
Color col = !alive ? Color{120, 120, 126, 255}
|
||||||
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
: t == SettleTier::City ? Color{250, 220, 110, 255}
|
||||||
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
: t == SettleTier::Town ? Color{225, 170, 90, 255}
|
||||||
: Color{210, 130, 85, 255};
|
: Color{210, 130, 85, 255};
|
||||||
col = witherColor(col, k < cond.size() ? cond[k] : 1.0);
|
col = witherColor(col, k < cond.size() ? cond[k] : 1.0);
|
||||||
DrawCircleV(sp, rad, col);
|
DrawCircleV(sp, rad, col);
|
||||||
if (alive && t != SettleTier::Village) DrawCircleLines((int)sp.x, (int)sp.y, rad + 2.0f, Color{255, 245, 210, 180});
|
if (alive && t != SettleTier::Village) DrawCircleLines((int)sp.x, (int)sp.y, rad + 2.0f * scale, Color{255, 245, 210, 180});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (phase3 && showRivers) {
|
if (phase3 && showRivers) {
|
||||||
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f, vr, mapLon);
|
drawSegments2D(rivers, Color{80, 170, 235, 255}, 1.5f * scale, vr, lonOffset);
|
||||||
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f, vr, mapLon);
|
drawSegments2D(bigRivers, Color{80, 170, 235, 255}, 3.0f * scale, vr, lonOffset);
|
||||||
}
|
}
|
||||||
if (showDrift && !plateLabels.empty()) {
|
if (showDrift && !plateLabels.empty()) {
|
||||||
|
int fs = (int)std::round(12 * scale);
|
||||||
for (const auto& lbl : plateLabels) {
|
for (const auto& lbl : plateLabels) {
|
||||||
Vec3 u = Vec3{lbl.pos.x, lbl.pos.y, lbl.pos.z}.normalized();
|
Vec3 u = Vec3{lbl.pos.x, lbl.pos.y, lbl.pos.z}.normalized();
|
||||||
double lon, lat; dirToLonLat(u, lon, lat);
|
double lon, lat; dirToLonLat(u, lon, lat);
|
||||||
Vector2 lp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 lp = projLonLat(lon, lat, lonOffset, vr);
|
||||||
const char* txt = TextFormat("P%d", lbl.id);
|
const char* txt = TextFormat("P%d", lbl.id);
|
||||||
DrawText(txt, (int)lp.x + 4, (int)lp.y - 8, 12, RAYWHITE);
|
DrawText(txt, (int)(lp.x + 4 * scale), (int)(lp.y - 8 * scale), fs, RAYWHITE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Place-name labels (the atlas). Minor features only when the map is zoomed in.
|
// Place-name labels (the atlas). Minor features only when the map is zoomed in (on-screen) or
|
||||||
|
// always for a high-res export, where there's ample room for the full detail.
|
||||||
if (showNames && planet.geographyBuilt()) {
|
if (showNames && planet.geographyBuilt()) {
|
||||||
bool zoomed = mapZoom > 1.5;
|
|
||||||
for (const GeoFeature& f : planet.geography()) {
|
for (const GeoFeature& f : planet.geography()) {
|
||||||
if (f.anchorCell < 0 || f.anchorCell >= (int)planet.cells.size()) continue;
|
if (f.anchorCell < 0 || f.anchorCell >= (int)planet.cells.size()) continue;
|
||||||
int font; Color col; bool minor = labelStyle(f, font, col);
|
int font; Color col; bool minor = labelStyle(f, font, col);
|
||||||
if (font <= 0 || (minor && !zoomed)) continue;
|
if (font <= 0 || (minor && !showMinorLabels)) continue;
|
||||||
|
font = (int)std::round(font * scale);
|
||||||
|
if (font <= 0) continue;
|
||||||
double lon, lat; dirToLonLat(planet.cells[f.anchorCell].unit, lon, lat);
|
double lon, lat; dirToLonLat(planet.cells[f.anchorCell].unit, lon, lat);
|
||||||
Vector2 lp = projLonLat(lon, lat, mapLon, vr);
|
Vector2 lp = projLonLat(lon, lat, lonOffset, vr);
|
||||||
if (!CheckCollisionPointRec(lp, mapRect)) continue;
|
if (!CheckCollisionPointRec(lp, vr)) continue;
|
||||||
int w = MeasureText(f.name.c_str(), font);
|
int w = MeasureText(f.name.c_str(), font);
|
||||||
DrawText(f.name.c_str(), (int)lp.x - w / 2 + 1, (int)lp.y - font / 2 + 1, font, Color{0, 0, 0, 180});
|
DrawText(f.name.c_str(), (int)lp.x - w / 2 + 1, (int)lp.y - font / 2 + 1, font, Color{0, 0, 0, 180});
|
||||||
DrawText(f.name.c_str(), (int)lp.x - w / 2, (int)lp.y - font / 2, font, col);
|
DrawText(f.name.c_str(), (int)lp.x - w / 2, (int)lp.y - font / 2, font, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewer::renderMap2D() {
|
||||||
|
DrawRectangleRec(mapRect, Color{6, 8, 14, 255});
|
||||||
|
Rectangle vr = mapViewRect(); // projection rect (zoom/pan); scissor stays mapRect
|
||||||
|
BeginScissorMode((int)mapRect.x, (int)mapRect.y, (int)mapRect.width, (int)mapRect.height);
|
||||||
|
drawMap2D(planet, displayColors(), map2D, vr, mapLon);
|
||||||
|
drawMapOverlays(vr, mapLon, 1.0f, (float)std::min(2.0, mapZoom), mapZoom > 1.5);
|
||||||
if (selectedCell >= 0) DrawCircleV(mapScreen(map2D, selectedCell, vr, mapLon), 5, ORANGE);
|
if (selectedCell >= 0) DrawCircleV(mapScreen(map2D, selectedCell, vr, mapLon), 5, ORANGE);
|
||||||
if (hovered >= 0) DrawCircleV(mapScreen(map2D, hovered, vr, mapLon), 4, YELLOW);
|
if (hovered >= 0) DrawCircleV(mapScreen(map2D, hovered, vr, mapLon), 4, YELLOW);
|
||||||
EndScissorMode();
|
EndScissorMode();
|
||||||
@ -485,6 +497,38 @@ void Viewer::renderMap2D() {
|
|||||||
(int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255});
|
(int)mapRect.x + 6, (int)mapRect.y + 4, 14, Color{200, 200, 210, 255});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renders the full (unzoomed) 2D Equal Earth map at a high resolution into an off-screen
|
||||||
|
// RenderTexture2D and saves it as a timestamped PNG -- a "poster" export independent of the
|
||||||
|
// current on-screen window/zoom. Keeps the current colour mode, longitude pan and overlay
|
||||||
|
// toggles (so it exports "what you're looking at", just bigger); minor place-name labels are
|
||||||
|
// always shown since a 4K canvas has ample room for them. Uses the existing `map2D` (its per-cell
|
||||||
|
// lon/lat don't depend on the target rect -- only screen position does, recomputed per rect by
|
||||||
|
// drawMapTris/projLonLat), so no rebuild is needed.
|
||||||
|
void Viewer::exportMapImage() {
|
||||||
|
const int exportW = 3840; // ~4K wide
|
||||||
|
int exportH = std::max(1, (int)std::lround(exportW * (double)mapRect.height / (double)mapRect.width));
|
||||||
|
float scale = exportW / mapRect.width;
|
||||||
|
|
||||||
|
RenderTexture2D rt = LoadRenderTexture(exportW, exportH);
|
||||||
|
Rectangle er{ 0, 0, (float)exportW, (float)exportH };
|
||||||
|
BeginTextureMode(rt);
|
||||||
|
ClearBackground(Color{6, 8, 14, 255});
|
||||||
|
drawMap2D(planet, displayColors(), map2D, er, mapLon);
|
||||||
|
drawMapOverlays(er, mapLon, scale, scale, /*showMinorLabels=*/true);
|
||||||
|
EndTextureMode();
|
||||||
|
|
||||||
|
Image img = LoadImageFromTexture(rt.texture);
|
||||||
|
ImageFlipVertical(&img); // render textures are stored bottom-up
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm tmv; localtime_r(&now, &tmv);
|
||||||
|
char name[64];
|
||||||
|
strftime(name, sizeof(name), "map_export_%Y-%m-%d_%H%M%S.png", &tmv);
|
||||||
|
ExportImage(img, name);
|
||||||
|
UnloadImage(img);
|
||||||
|
UnloadRenderTexture(rt);
|
||||||
|
setStatus(TextFormat("Map exported to %s (%dx%d)", name, exportW, exportH));
|
||||||
|
}
|
||||||
|
|
||||||
// Live World tabbed info panel in the freed space right of the (left-aligned) 2D map.
|
// Live World tabbed info panel in the freed space right of the (left-aligned) 2D map.
|
||||||
void Viewer::renderLiveInfo() {
|
void Viewer::renderLiveInfo() {
|
||||||
liveInfoTabRects.clear(); eventRowRects.clear(); eventRowIndices.clear(); atlasRowCells.clear();
|
liveInfoTabRects.clear(); eventRowRects.clear(); eventRowIndices.clear(); atlasRowCells.clear();
|
||||||
@ -1072,6 +1116,9 @@ void Viewer::renderFrame() {
|
|||||||
renderPanels();
|
renderPanels();
|
||||||
renderHUD();
|
renderHUD();
|
||||||
drawToolbar(*this); // drawn after the HUD, before the modal so it dims/locks together with it
|
drawToolbar(*this); // drawn after the HUD, before the modal so it dims/locks together with it
|
||||||
|
// Run after drawToolbar() fully returns (its own scissor mode is guaranteed closed by then) --
|
||||||
|
// exportMapImage()'s render-texture pass must not inherit the toolbar's scroll-content clip rect.
|
||||||
|
if (pendingMapExport) { pendingMapExport = false; exportMapImage(); }
|
||||||
renderPrompt();
|
renderPrompt();
|
||||||
|
|
||||||
EndDrawing();
|
EndDrawing();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user