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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
2026-07-23 07:15:05 +02:00
parent 4baab16794
commit 3d27213997
2 changed files with 48 additions and 36 deletions

View File

@@ -101,9 +101,14 @@ const WorldIconEntry kWorldIconFiles[] = {
{ BuildingType::EnemyDefenceStation, "station.svg" }, { BuildingType::EnemyDefenceStation, "station.svg" },
}; };
// Renders an icon SVG's glyph only (the chip background rect stripped) into a // On-screen size of every world icon, as a multiple of one tile (REQ-UI-WORLD-ICON):
// transparent pixmap, recoloring the white glyph stroke to inkHex. // a little over one tile so all buildings' icons read at the same size regardless
QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex) // 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); QString s = QString::fromUtf8(svg);
// Drop the full-canvas chip rect only; some glyphs use their own <rect> // Drop the full-canvas chip rect only; some glyphs use their own <rect>
@@ -113,14 +118,7 @@ QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex)
QStringLiteral("<rect\\s+width=\"100\"\\s+height=\"100\"[^>]*>")); QStringLiteral("<rect\\s+width=\"100\"\\s+height=\"100\"[^>]*>"));
s.remove(backgroundRect); s.remove(backgroundRect);
s.replace(QStringLiteral("#ffffff"), inkHex); s.replace(QStringLiteral("#ffffff"), inkHex);
return s.toUtf8();
const int px = 192;
QPixmap pixmap(px, px);
pixmap.fill(Qt::transparent);
QSvgRenderer renderer(s.toUtf8());
QPainter painter(&pixmap);
renderer.render(&painter);
return pixmap;
} }
// Perceived luminance test; picks a dark glyph on light fills so it stays legible. // 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. continue; // A missing icon is not an error; the text glyph is used.
} }
const QByteArray svg = file.readAll(); const QByteArray svg = file.readAll();
BuildingIconPixmaps pixmaps; BuildingIconRenderers renderers;
pixmaps.white = renderWorldIcon(svg, QStringLiteral("#ffffff")); renderers.white = std::make_unique<QSvgRenderer>(
pixmaps.dark = renderWorldIcon(svg, QStringLiteral("#1c1c20")); worldIconSvg(svg, QStringLiteral("#ffffff")));
m_buildingIcons[entry.type] = pixmaps; renderers.dark = std::make_unique<QSvgRenderer>(
worldIconSvg(svg, QStringLiteral("#1c1c20")));
m_buildingIcons[entry.type] = std::move(renderers);
} }
} }
bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type, bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
const QRectF& box, const QColor& fill) const const QRectF& box, const QColor& fill) const
{ {
const std::map<BuildingType, BuildingIconPixmaps>::const_iterator it = const std::map<BuildingType, BuildingIconRenderers>::const_iterator it =
m_buildingIcons.find(type); m_buildingIcons.find(type);
if (it == m_buildingIcons.end()) { return false; } if (it == m_buildingIcons.end()) { return false; }
// Centered square keeps the glyph undistorted on non-square footprints; the // Every icon is the same fixed on-screen size, centered on the footprint,
// icon's own 13% margin supplies padding. // regardless of footprint size (REQ-UI-WORLD-ICON).
const qreal side = std::min(box.width(), box.height()); const qreal side = static_cast<qreal>(getTilePx()) * kWorldIconTileFactor;
const QRectF target(box.center().x() - side / 2.0, const QRectF target(box.center().x() - side / 2.0,
box.center().y() - side / 2.0, side, side); 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); // Render the glyph as vector at the view scale so it stays crisp (a
painter.drawPixmap(target, pixmap, pixmap.rect()); // 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; return true;
} }
@@ -1426,19 +1431,20 @@ void GameWorldView::drawBuildings(QPainter& painter)
const QString pctText = QString::number(pct) + "%"; const QString pctText = QString::number(pct) + "%";
painter.setPen(bv.outline); painter.setPen(bv.outline);
const QRectF topHalf(bboxRect.x(), bboxRect.y(), // Identity symbol with the progress percentage below it
bboxRect.width(), bboxRect.height() * 0.5); // (REQ-UI-CONSTRUCTION-PROGRESS): the world icon centered on the
const QRectF botHalf(bboxRect.x(), // footprint where the type has one, otherwise the text glyph.
bboxRect.y() + bboxRect.height() * 0.5, if (drawBuildingIcon(painter, s.type, bboxRect, bv.fill))
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))
{ {
painter.drawText(botHalf, Qt::AlignCenter, pctText); painter.drawText(bboxRect, Qt::AlignHCenter | Qt::AlignBottom, pctText);
} }
else if (!bv.glyph.isEmpty()) 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(topHalf, Qt::AlignCenter, bv.glyph);
painter.drawText(botHalf, Qt::AlignCenter, pctText); painter.drawText(botHalf, Qt::AlignCenter, pctText);
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <map> #include <map>
#include <memory>
#include <optional> #include <optional>
#include <random> #include <random>
#include <set> #include <set>
@@ -11,7 +12,6 @@
#include <QColor> #include <QColor>
#include <QElapsedTimer> #include <QElapsedTimer>
#include <QOpenGLWidget> #include <QOpenGLWidget>
#include <QPixmap>
#include <QPoint> #include <QPoint>
#include <QRectF> #include <QRectF>
#include <QTimer> #include <QTimer>
@@ -54,6 +54,7 @@ struct ParsedReplay;
class ReplayPlayer; class ReplayPlayer;
class Simulation; class Simulation;
class QPainter; class QPainter;
class QSvgRenderer;
struct QPointCompare struct QPointCompare
{ {
@@ -284,11 +285,16 @@ private:
const GameConfig* m_config; const GameConfig* m_config;
const VisualsConfig* m_visuals; 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 // white and a dark variant so drawBuildingIcon can auto-contrast against the
// building's fill. Populated once by loadBuildingIcons(). // building's fill. Rendered as vector at the view scale each draw so they
struct BuildingIconPixmaps { QPixmap white; QPixmap dark; }; // stay crisp. Populated once by loadBuildingIcons().
std::map<BuildingType, BuildingIconPixmaps> m_buildingIcons; struct BuildingIconRenderers
{
std::unique_ptr<QSvgRenderer> white;
std::unique_ptr<QSvgRenderer> dark;
};
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
// Funnels all player input into the single Simulation::apply chokepoint. // Funnels all player input into the single Simulation::apply chokepoint.
CommandManager m_commandManager; CommandManager m_commandManager;