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" },
};
// 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 <rect>
@@ -113,14 +118,7 @@ QPixmap renderWorldIcon(const QByteArray& svg, const QString& inkHex)
QStringLiteral("<rect\\s+width=\"100\"\\s+height=\"100\"[^>]*>"));
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<QSvgRenderer>(
worldIconSvg(svg, QStringLiteral("#ffffff")));
renderers.dark = std::make_unique<QSvgRenderer>(
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<BuildingType, BuildingIconPixmaps>::const_iterator it =
const std::map<BuildingType, BuildingIconRenderers>::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<qreal>(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);
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include <map>
#include <memory>
#include <optional>
#include <random>
#include <set>
@@ -11,7 +12,6 @@
#include <QColor>
#include <QElapsedTimer>
#include <QOpenGLWidget>
#include <QPixmap>
#include <QPoint>
#include <QRectF>
#include <QTimer>
@@ -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<BuildingType, BuildingIconPixmaps> 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<QSvgRenderer> white;
std::unique_ptr<QSvgRenderer> dark;
};
std::map<BuildingType, BuildingIconRenderers> m_buildingIcons;
// Funnels all player input into the single Simulation::apply chokepoint.
CommandManager m_commandManager;