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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user