From 3d27213997164cbd27ac8416b835aac8d24b861e Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Thu, 23 Jul 2026 07:15:05 +0200 Subject: [PATCH] Make world building icons uniform size and crisp Draw every world building icon at a fixed size (1.25x tile, centered on the footprint) so all buildings' icons read at the same size regardless of footprint, and a little larger than before. Render the icon as vector via a cached QSvgRenderer at the view scale each draw instead of downscaling a pre-rasterized pixmap, which was blurry. Center the construction-site icon on the footprint with the progress percentage bottom-aligned. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc --- src/ui/GameWorldView.cpp | 68 ++++++++++++++++++++++------------------ src/ui/GameWorldView.h | 16 +++++++--- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index ab55e45..b63fafe 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -101,9 +101,14 @@ const WorldIconEntry kWorldIconFiles[] = { { BuildingType::EnemyDefenceStation, "station.svg" }, }; -// Renders an icon SVG's glyph only (the chip background rect stripped) into a -// transparent pixmap, recoloring the white glyph stroke to inkHex. -QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex) +// On-screen size of every world icon, as a multiple of one tile (REQ-UI-WORLD-ICON): +// a little over one tile so all buildings' icons read at the same size regardless +// of footprint. +const qreal kWorldIconTileFactor = 1.25; + +// Produces an icon SVG containing only the glyph (the chip background rect +// stripped), with the white glyph stroke recolored to inkHex. +QByteArray worldIconSvg(const QByteArray& svg, const QString& inkHex) { QString s = QString::fromUtf8(svg); // Drop the full-canvas chip rect only; some glyphs use their own @@ -113,14 +118,7 @@ QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex) QStringLiteral("]*>")); s.remove(backgroundRect); s.replace(QStringLiteral("#ffffff"), inkHex); - - const int px = 192; - QPixmap pixmap(px, px); - pixmap.fill(Qt::transparent); - QSvgRenderer renderer(s.toUtf8()); - QPainter painter(&pixmap); - renderer.render(&painter); - return pixmap; + return s.toUtf8(); } // Perceived luminance test; picks a dark glyph on light fills so it stays legible. @@ -1279,29 +1277,36 @@ void GameWorldView::loadBuildingIcons(const std::string& configDir) continue; // A missing icon is not an error; the text glyph is used. } const QByteArray svg = file.readAll(); - BuildingIconPixmaps pixmaps; - pixmaps.white = renderWorldIcon(svg, QStringLiteral("#ffffff")); - pixmaps.dark = renderWorldIcon(svg, QStringLiteral("#1c1c20")); - m_buildingIcons[entry.type] = pixmaps; + BuildingIconRenderers renderers; + renderers.white = std::make_unique( + worldIconSvg(svg, QStringLiteral("#ffffff"))); + renderers.dark = std::make_unique( + worldIconSvg(svg, QStringLiteral("#1c1c20"))); + m_buildingIcons[entry.type] = std::move(renderers); } } bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type, const QRectF& box, const QColor& fill) const { - const std::map::const_iterator it = + const std::map::const_iterator it = m_buildingIcons.find(type); if (it == m_buildingIcons.end()) { return false; } - // Centered square keeps the glyph undistorted on non-square footprints; the - // icon's own 13% margin supplies padding. - const qreal side = std::min(box.width(), box.height()); + // Every icon is the same fixed on-screen size, centered on the footprint, + // regardless of footprint size (REQ-UI-WORLD-ICON). + const qreal side = static_cast(getTilePx()) * kWorldIconTileFactor; const QRectF target(box.center().x() - side / 2.0, box.center().y() - side / 2.0, side, side); - const QPixmap& pixmap = isLightFill(fill) ? it->second.dark : it->second.white; + QSvgRenderer* renderer = isLightFill(fill) ? it->second.dark.get() + : it->second.white.get(); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - painter.drawPixmap(target, pixmap, pixmap.rect()); + // Render the glyph as vector at the view scale so it stays crisp (a + // pre-rasterized pixmap downscaled to tile size looked blurry). + const bool wasAntialiasing = painter.testRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::Antialiasing, true); + renderer->render(&painter, target); + painter.setRenderHint(QPainter::Antialiasing, wasAntialiasing); return true; } @@ -1426,19 +1431,20 @@ void GameWorldView::drawBuildings(QPainter& painter) const QString pctText = QString::number(pct) + "%"; painter.setPen(bv.outline); - const QRectF topHalf(bboxRect.x(), bboxRect.y(), - bboxRect.width(), bboxRect.height() * 0.5); - const QRectF botHalf(bboxRect.x(), - bboxRect.y() + bboxRect.height() * 0.5, - bboxRect.width(), bboxRect.height() * 0.5); - // Identity symbol above, progress percentage below (REQ-UI-CONSTRUCTION-PROGRESS): - // the world icon where the type has one, otherwise the text glyph. - if (drawBuildingIcon(painter, s.type, topHalf, bv.fill)) + // Identity symbol with the progress percentage below it + // (REQ-UI-CONSTRUCTION-PROGRESS): the world icon centered on the + // footprint where the type has one, otherwise the text glyph. + if (drawBuildingIcon(painter, s.type, bboxRect, bv.fill)) { - painter.drawText(botHalf, Qt::AlignCenter, pctText); + painter.drawText(bboxRect, Qt::AlignHCenter | Qt::AlignBottom, pctText); } else if (!bv.glyph.isEmpty()) { + const QRectF topHalf(bboxRect.x(), bboxRect.y(), + bboxRect.width(), bboxRect.height() * 0.5); + const QRectF botHalf(bboxRect.x(), + bboxRect.y() + bboxRect.height() * 0.5, + bboxRect.width(), bboxRect.height() * 0.5); painter.drawText(topHalf, Qt::AlignCenter, bv.glyph); painter.drawText(botHalf, Qt::AlignCenter, pctText); } diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index c24ff22..cf7d4a0 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -54,6 +54,7 @@ struct ParsedReplay; class ReplayPlayer; class Simulation; class QPainter; +class QSvgRenderer; struct QPointCompare { @@ -284,11 +285,16 @@ private: const GameConfig* m_config; const VisualsConfig* m_visuals; - // Pre-rendered world icon glyphs per building type (REQ-UI-WORLD-ICON), in a + // World icon glyph renderers per building type (REQ-UI-WORLD-ICON), in a // white and a dark variant so drawBuildingIcon can auto-contrast against the - // building's fill. Populated once by loadBuildingIcons(). - struct BuildingIconPixmaps { QPixmap white; QPixmap dark; }; - std::map m_buildingIcons; + // building's fill. Rendered as vector at the view scale each draw so they + // stay crisp. Populated once by loadBuildingIcons(). + struct BuildingIconRenderers + { + std::unique_ptr white; + std::unique_ptr dark; + }; + std::map m_buildingIcons; // Funnels all player input into the single Simulation::apply chokepoint. CommandManager m_commandManager;