From 74874bb81e13553572a3f695bff27075b029aa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 19 Sep 2025 10:24:54 -0600 Subject: [PATCH] Test a multi-shape image --- .gitmodules | 2 +- Common/Render/AtlasGen.cpp | 6 ++- Common/Render/AtlasGen.h | 4 ++ UI/UIAtlas.cpp | 63 +++++++++++++++++-------- assets/ui_images/images.svg | 94 ++++++++++++++++++++++++++++++++++++- ext/nanosvg | 2 +- 6 files changed, 147 insertions(+), 24 deletions(-) diff --git a/.gitmodules b/.gitmodules index 681e27ab05..878704d5ea 100644 --- a/.gitmodules +++ b/.gitmodules @@ -64,4 +64,4 @@ url = https://github.com/hrydgard/freetype.git [submodule "ext/nanosvg"] path = ext/nanosvg - url = git@github.com:memononen/nanosvg.git + url = https://github.com/hrydgard/nanosvg.git diff --git a/Common/Render/AtlasGen.cpp b/Common/Render/AtlasGen.cpp index 115e7eba46..784462f447 100644 --- a/Common/Render/AtlasGen.cpp +++ b/Common/Render/AtlasGen.cpp @@ -80,6 +80,7 @@ void Bucket::AddImage(Image &&img, int id) { dat.ey = (int)img.height(); dat.w = dat.ex; dat.h = dat.ey; + dat.scale = img.scale; dat.redToWhiteAlpha = false; images.emplace_back(std::move(img)); data.push_back(dat); @@ -173,8 +174,9 @@ AtlasImage ToAtlasImage(int id, std::string_view name, float tw, float th, const img.v1 = results[i].sy / th + toffy; img.u2 = results[i].ex / tw - toffx; img.v2 = results[i].ey / th - toffy; - img.w = results[i].ex - results[i].sx; - img.h = results[i].ey - results[i].sy; + // The w and h here is the UI-pixels width/height. So if we rasterized at another DPI than 1.0f, we need to scale here. + img.w = results[i].w / results[i].scale; + img.h = results[i].h / results[i].scale; truncate_cpy(img.name, name); return img; } diff --git a/Common/Render/AtlasGen.h b/Common/Render/AtlasGen.h index 051ede035b..44159b6b26 100644 --- a/Common/Render/AtlasGen.h +++ b/Common/Render/AtlasGen.h @@ -33,6 +33,8 @@ struct Image { Image(Image &&) = default; Image &operator=(Image &&) = default; + float scale = 1.0f; + int w = 0; int h = 0; @@ -82,6 +84,8 @@ struct Data { // distance to move the origin forward float wx; + float scale; + bool redToWhiteAlpha; int charNum; }; diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp index fe661e8e92..498d64d387 100644 --- a/UI/UIAtlas.cpp +++ b/UI/UIAtlas.cpp @@ -159,7 +159,21 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas) { // There's a couple of approaches here, either we can pick apart the SVG and render each piece separately, // or we just rasterize the whole thing in one go and use the bounding boxes to pick out the sub-images. // We'll start with the latter, although the momentary memory requirements are higher. - std::vector usedShapes; + struct UsedShape { + float minX = 1000000.0f; + float maxX = -1000000.0f; + float minY = 1000000.0f; + float maxY = -1000000.0f; + + void Merge(NSVGshape *shape) { + if (shape->bounds[0] < minX) minX = shape->bounds[0]; + if (shape->bounds[1] < minY) minY = shape->bounds[1]; + if (shape->bounds[2] > maxX) maxX = shape->bounds[2]; + if (shape->bounds[3] > maxY) maxY = shape->bounds[3]; + } + }; + + std::map usedShapes; if (image) { // Loop through the shapes to list them, and to hide them if irrelevant. NSVGshape *shape = image->shapes; @@ -169,8 +183,12 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas) { INFO_LOG(Log::G3D, "Ignoring shape %s", shape->id); shape->flags &= ~NSVG_FLAGS_VISIBLE; } else { - INFO_LOG(Log::G3D, "Found shape: %s (%0.2f %0.2f %0.2f %0.2f)", shape->id, shape->bounds[0], shape->bounds[1], shape->bounds[2], shape->bounds[3]); - usedShapes.push_back(shape); + if (usedShapes.find(shape->id) != usedShapes.end()) { + INFO_LOG(Log::G3D, "Duplicate shape ID in SVG, merging bboxes: %s", shape->id); + } else { + INFO_LOG(Log::G3D, "Found shape: %s (%0.2f %0.2f %0.2f %0.2f)", shape->id, shape->bounds[0], shape->bounds[1], shape->bounds[2], shape->bounds[3]); + } + usedShapes[shape->id].Merge(shape); } shape = shape->next; } @@ -180,46 +198,53 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas) { // Rasterize here, and add into image list. rast = nsvgCreateRasterizer(); - INFO_LOG(Log::G3D, "Rasterizing SVG: %d x %d", (int)image->width, (int)image->height); + float scale = 2.0f; + int svgWidth = image->width * scale; + int svgHeight = image->height * scale; - char *svgImg = new char[(int)image->width * (int)image->height * 4]; - memset(svgImg, 0, (int)image->width * (int)image->height * 4); - nsvgRasterize(rast, image, 0, 0, 1.0f, (unsigned char *)svgImg, (int)image->width, (int)image->height, (int)image->width * 4); + INFO_LOG(Log::G3D, "Rasterizing SVG: %d x %d at scale %0.2f", svgWidth, svgHeight, scale); + + char *svgImg = new char[svgWidth * svgHeight * 4]; + memset(svgImg, 0, svgWidth * svgHeight * 4); + nsvgRasterize(rast, image, 0, 0, scale, (unsigned char *)svgImg, svgWidth, svgHeight, svgWidth * 4); // Now, loop through the shapes again and copy out the ones we care about. - for (auto shape : usedShapes) { - int index = GetImageIndex(shape->id); + for (auto &[shapeId, bounds] : usedShapes) { + int index = GetImageIndex(shapeId); _dbg_assert_(index != -1); if (index == -1) { continue; } Image &img = images[index]; - int minX = std::max(0, (int)floorf(shape->bounds[0])); - int minY = std::max(0, (int)floorf(shape->bounds[1])); - int maxX = std::min((int)image->width, (int)ceilf(shape->bounds[2])); - int maxY = std::min((int)image->height, (int)ceilf(shape->bounds[3])); + int minX = std::max(0, (int)floorf(bounds.minX * scale)); + int minY = std::max(0, (int)floorf(bounds.minY * scale)); + int maxX = std::min(svgWidth, (int)ceilf(bounds.maxX * scale)); + int maxY = std::min(svgHeight, (int)ceilf(bounds.maxY * scale)); int w = maxX - minX; int h = maxY - minY; if (w <= 0 || h <= 0) { - ERROR_LOG(Log::G3D, "Invalid size for %s: %dx%d", shape->id, w, h); + ERROR_LOG(Log::G3D, "Invalid size for %s: %dx%d", shapeId.c_str(), w, h); continue; } img.resize(w, h); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { - int sx = (int)(shape->bounds[0] + x); - int sy = (int)(shape->bounds[1] + y); - const u32 *src = (u32 *)svgImg + (sy * (int)image->width + sx); + int sx = minX + x; + int sy = minY + y; + const u32 *src = (u32 *)svgImg + (sy * svgWidth + sx); u32 col = *src; img.set1(x, y, col); } } + img.scale = scale; + // pngSave(Path(std::string("../buttons_") + PNGNameFromID(shape->id)), img.data(), img.width(), img.height(), 4); } - // pngSave(Path("../buttons_rasterized.png"), svgImg, (int)image->width, (int)image->height, 4); + + // pngSave(Path("../buttons_rasterized.png"), svgImg, svgWidth, svgHeight, 4); delete[] svgImg; nsvgDeleteRasterizer(rast); @@ -227,7 +252,7 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas) { } } - INFO_LOG(Log::G3D, " - Rasterized svg image in %.2f ms\n", images.size(), svgStart.ElapsedMs()); + INFO_LOG(Log::G3D, " - Rasterized svg image in %0.2f ms\n", svgStart.ElapsedMs()); Instant pngStart = Instant::Now(); diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index d5767b70e1..5c990c0937 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" inkscape:version="1.4.2 (f4327f4, 2025-05-13)" - sodipodi:docname="buttons.svg" + sodipodi:docname="images.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -178,6 +178,76 @@ operator="over" result="composite2" /> + + + + + + + + + + + + + + + + + + diff --git a/ext/nanosvg b/ext/nanosvg index ea6a6aca00..478dbb8f7e 160000 --- a/ext/nanosvg +++ b/ext/nanosvg @@ -1 +1 @@ -Subproject commit ea6a6aca009422bba0dbad4c80df6e6ba0c82183 +Subproject commit 478dbb8f7ed11c3d9b20b3986a8ee2283f483ef7