From 9b0e88db7a26507f7117ebcac11938d7a50b6bec Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 8 Oct 2026 23:55:56 +0200 Subject: [PATCH] feat(editor): node bank shows the entries as cards with the previews of their models (Previews, size of the cards 48-256 px, kept in eu07_editor.ini), in the groups and with the search as the list; images found by the names the preview generator gives them, worked out by the same code now (editor/editorPreviews: path of the model and skin, numbers of entries with the same ones in the order of nodebank.txt), read and scaled down in a thread of their own, uploaded a few in a frame, at most 512 kept, read anew on Reload node bank; images of the user interface made by the gl renderers (Create_Image/Release_Image of the imgui renderer, srgb in the modern one); Polish translation --- CMakeLists.txt | 1 + application/editoruipanels.cpp | 185 ++++++++++++++++- application/editoruipanels.h | 9 + editor/editorPreviews.cpp | 345 +++++++++++++++++++++++++++++++ editor/editorPreviews.hpp | 94 +++++++++ editor/editorSettings.cpp | 6 + editor/editorSettings.hpp | 8 + lang/editor_pl.po | 18 ++ nodebank_previews_standalone.cpp | 72 +------ rendering/opengl33renderer.cpp | 30 +++ rendering/opengl33renderer.h | 2 + rendering/openglrenderer.cpp | 26 +++ rendering/openglrenderer.h | 2 + rendering/renderer.h | 7 + 14 files changed, 737 insertions(+), 68 deletions(-) create mode 100644 editor/editorPreviews.cpp create mode 100644 editor/editorPreviews.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f90db3b..9f52e785 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,6 +184,7 @@ set(SOURCES "editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" +"editor/editorPreviews.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormodetrackux.cpp" diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 2381d40f..b7f8d803 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -23,6 +23,11 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "editor/editorModelSets.hpp" #include "editor/editorFormat.hpp" +#include "editor/editorSettings.hpp" +#include "imgui/imgui_internal.h" + +#include +#include namespace { @@ -544,6 +549,7 @@ nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_ m_nodebank.push_back({label, std::make_shared(nodedata)}); } + index_previews(); // sort alphabetically content of each group auto groupbegin{m_nodebank.begin()}; auto groupend{groupbegin}; @@ -557,6 +563,11 @@ nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_ void nodebank_panel::nodebank_reload() { m_nodebank.clear(); + // images made since are read anew + if (m_previews) + { + m_previews->clear(); + } std::ifstream file; file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary); std::string line; @@ -572,6 +583,7 @@ void nodebank_panel::nodebank_reload() m_nodebank.push_back({label, std::make_shared(nodedata)}); } + index_previews(); // sort alphabetically content of each group auto groupbegin{m_nodebank.begin()}; auto groupend{groupbegin}; @@ -583,6 +595,110 @@ void nodebank_panel::nodebank_reload() } } +void nodebank_panel::index_previews() +{ + m_previewpaths.clear(); + std::error_code error; + m_previewfolder = std::filesystem::is_directory(editor_previews::folder, error); + // entries with the same model and skin are numbered in the order of the file, the way the generator numbers them + std::set taken; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + { + continue; + } + auto const path{editor_previews::entry_preview_path(*entry.second)}; + if (path.empty()) + { + continue; + } + auto const name{editor_previews::variant(path, taken)}; + taken.insert(name); + m_previewpaths[entry.second.get()] = name + ".png"; + } +} + +void nodebank_panel::render_cards(std::vector> const *> const &Entries) +{ + auto const &style{ImGui::GetStyle()}; + auto const card{std::max(16.0f, EditorSettings.nodebank_card() * Global.ui_scale)}; + auto const labelheight{ImGui::GetTextLineHeight()}; + ImVec2 const cell{card, card + labelheight + style.FramePadding.y * 2.0f}; + auto const columns{std::max(1, static_cast((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (cell.x + style.ItemSpacing.x)))}; + auto const rows{(static_cast(Entries.size()) + columns - 1) / columns}; + ImGuiListClipper clipper; + clipper.Begin(rows, cell.y + style.ItemSpacing.y); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + for (int column = 0; column < columns; ++column) + { + auto const index{static_cast(row * columns + column)}; + if (index >= Entries.size()) + { + break; + } + auto const &entry{*Entries[index]}; + if (column > 0) + { + ImGui::SameLine(); + } + ImGui::PushID(entry.second.get()); + auto const position{ImGui::GetCursorScreenPos()}; + if (ImGui::InvisibleButton("##card", cell)) + { + m_selectedtemplate = entry.second; + } + auto const hovered{ImGui::IsItemHovered()}; + auto const selected{entry.second == m_selectedtemplate}; + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const end{position.x + cell.x, position.y + cell.y}; + list->AddRectFilled(position, end, ImGui::GetColorU32(selected ? ImGuiCol_Header : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg), style.FrameRounding); + // the image fills the square of the card; without one, what's missing is said in its place + ImVec2 const imagemin{position.x + 2.0f, position.y + 2.0f}; + ImVec2 const imagemax{position.x + card - 2.0f, position.y + card - 2.0f}; + auto const lookup{m_previewpaths.find(entry.second.get())}; + std::uint64_t texture{0}; + char const *placeholder{STR_C("not a model")}; + if (lookup != m_previewpaths.end()) + { + texture = m_previews->image(lookup->second); + placeholder = (m_previews->missing(lookup->second) ? STR_C("no preview") : "..."); + } + if (texture != 0) + { + list->AddImage(static_cast(texture), imagemin, imagemax); + } + else + { + list->AddRect(imagemin, imagemax, ImGui::GetColorU32(ImGuiCol_Border), style.FrameRounding); + auto const size{ImGui::CalcTextSize(placeholder)}; + list->PushClipRect(imagemin, imagemax, true); + list->AddText(ImVec2(imagemin.x + std::max(0.0f, (imagemax.x - imagemin.x - size.x) * 0.5f), imagemin.y + (imagemax.y - imagemin.y - size.y) * 0.5f), ImGui::GetColorU32(ImGuiCol_TextDisabled), placeholder); + list->PopClipRect(); + } + // the label under the image, cut short with an ellipsis; the whole of it in the tooltip + auto const *label{entry.first.c_str()}; + while (*label == ' ') + { + ++label; + } + ImVec2 const labelmin{position.x + style.FramePadding.x, position.y + card + style.FramePadding.y}; + ImVec2 const labelmax{end.x - style.FramePadding.x, end.y}; + ImGui::RenderTextEllipsis(list, labelmin, labelmax, labelmax.x, labelmax.x, label, nullptr, nullptr); + if (hovered) + { + ImGui::SetTooltip("%s", label); + } + ImGui::PopID(); + } + } + } + clipper.End(); +} + void nodebank_panel::render() { if (false == is_open) @@ -617,6 +733,28 @@ void nodebank_panel::render() { m_setsopen = !m_setsopen; } + // cards with the previews of the models, or the list + ImGui::SameLine(); + auto previews{EditorSettings.nodebank_previews()}; + if (ImGui::Checkbox(STR_C("Previews"), &previews)) + { + EditorSettings.nodebank_previews(previews); + EditorSettings.save(); + } + if (previews) + { + ImGui::SameLine(); + auto card{EditorSettings.nodebank_card()}; + ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 120.0f * Global.ui_scale)); + if (ImGui::SliderInt("##cardsize", &card, 48, 256, STR_C("%d px"))) + { + EditorSettings.nodebank_card(card); + } + if (ImGui::IsItemDeactivatedAfterEdit()) + { + EditorSettings.save(); + } + } // the edit modes are chosen in the toolbar and set up in the tool options window, the gizmo and the node // properties are in the inspector: what's left here is the node bank itself @@ -625,7 +763,52 @@ void nodebank_panel::render() ImGui::InputTextWithHint(STR_C("Search"), STR_C("Search node bank"), m_nodesearch, IM_ARRAYSIZE(m_nodesearch)); // the list takes the rest of the window, but keeps a usable height when the sections above are expanded (the window scrolls then) auto const listheight{std::max(ImGui::GetContentRegionAvail().y, ImGui::GetTextLineHeightWithSpacing() * 10.0f)}; - if (ImGui::BeginListBox("##nodebank", ImVec2(-1, listheight))) + if (previews) + { + if (m_previews == nullptr) + { + m_previews = std::make_unique(); + } + m_previews->update(); + if (false == m_previewfolder) + { + ImGui::TextDisabled("%s", STR_C("No previews yet: they're made by eu07 -generate-nodebank-previews")); + } + } + if (previews && ImGui::BeginChild("##nodebankcards", ImVec2(-1, listheight), ImGuiChildFlags_Borders)) + { + // the groups as in the list, the entries of an open one as cards + auto isvisible{false}; + auto const searchfilter{std::string(m_nodesearch)}; + std::vector> const *> cards; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + { + if (false == cards.empty()) + { + render_cards(cards); + cards.clear(); + } + isvisible = ImGui::CollapsingHeader(entry.first.c_str()); + } + else if (isvisible && (searchfilter.empty() || contains(entry.first, searchfilter))) + { + cards.push_back(&entry); + } + } + if (false == cards.empty()) + { + render_cards(cards); + } + ImGui::Separator(); + ImGui::TextDisabled("%s", STR_C("Scenery templates (.inc) are listed with the previews off")); + } + if (previews) + { + ImGui::EndChild(); + } + else if (ImGui::BeginListBox("##nodebank", ImVec2(-1, listheight))) { auto idx{0}; auto isvisible{false}; diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 85d862b3..e2b081aa 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "application/uilayer.h" #include "utilities/Classes.h" #include "editor/editorIncludeInfo.hpp" +#include "editor/editorPreviews.hpp" #include /* @@ -161,12 +162,20 @@ class nodebank_panel : public ui_panel // methods: std::string generate_node_label(std::string Input) const; void render_sets_window(); + // finds the preview images of the entries, in the order of the file, as the generator names them + void index_previews(); + // entries of a group as cards with their previews, in rows as many as fit the width + void render_cards(std::vector> const *> const &Entries); // true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take bool node_selected() const; // members: std::vector>> m_nodebank; char m_nodesearch[128]; std::shared_ptr m_selectedtemplate; + // previews of the models: image of each node definition, by its address, and the images read so far + std::unordered_map m_previewpaths; + std::unique_ptr m_previews; + bool m_previewfolder{false}; // the generator was run, there's the folder of the images // set manager window bool m_setsopen{false}; int m_setsselected{0}; // id of the user set being edited diff --git a/editor/editorPreviews.cpp b/editor/editorPreviews.cpp new file mode 100644 index 00000000..67588930 --- /dev/null +++ b/editor/editorPreviews.cpp @@ -0,0 +1,345 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorPreviews.hpp" + +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" +#include "stb/stb_image.h" + +#include +#include + +namespace editor_previews +{ + +std::string const folder{"textures/previews/nodebank/"}; + +std::string file_name_part(std::string Text) +{ + for (auto &character : Text) + { + auto const c{static_cast(character)}; + if ((false == std::isalnum(c)) && (c != '_') && (c != '-') && (c != '.') && (c != '+') && (c != '(') && (c != ')')) + { + character = '_'; + } + } + if ((Text.empty()) || (Text == ".") || (Text == "..")) + { + Text = "_"; + } + return Text; +} + +std::string preview_path(std::string Model, std::string const &Skin) +{ + replace_slashes(Model); + erase_extension(Model); + std::string path; + std::size_t start{0}; + while (start <= Model.size()) + { + auto end{Model.find('/', start)}; + if (end == std::string::npos) + { + end = Model.size(); + } + if (end > start) + { + if (false == path.empty()) + { + path += '/'; + } + path += file_name_part(Model.substr(start, end - start)); + } + start = end + 1; + } + if ((false == Skin.empty()) && (Skin != "none")) + { + auto skin{Skin}; + replace_slashes(skin); + erase_extension(skin); + std::replace(skin.begin(), skin.end(), '/', '_'); + path += '@' + file_name_part(skin); + } + return folder + path; +} + +std::string entry_preview_path(std::string const &Entry) +{ + auto const nodestart{Entry.find("node")}; + if (nodestart == std::string::npos) + { + return {}; + } + auto const entry{Entry.substr(nodestart)}; + // node + cParser parser(entry, cParser::buffer_TEXT); + parser.getTokens(5); + std::string type; + for (int index = 0; index < 5; ++index) + { + parser >> type; + } + if (type != "model") + { + return {}; + } + // the model and skin are the first two tokens after the location and rotation, read as the generator reads them + cParser names(entry, cParser::buffer_TEXT); + names.getTokens(9, false); + auto const model{names.getToken()}; + auto const skin{names.getToken(false)}; + if (model.empty()) + { + return {}; + } + return preview_path(model, skin); +} + +std::string variant(std::string const &Path, std::set const &Taken) +{ + if (false == Taken.contains(Path)) + { + return Path; + } + auto number{2}; + while (Taken.contains(Path + "~" + std::to_string(number))) + { + ++number; + } + return Path + "~" + std::to_string(number); +} + +namespace +{ + +// images kept at most; at 128 pixels that's 32 MB +std::size_t const image_limit{512}; +// uploaded in a frame at most +int const uploads_per_frame{8}; + +} // namespace + +image_cache::image_cache() +{ + // at a bigger ui the images are drawn bigger too + m_size = std::clamp(static_cast(128.0f * std::max(1.0f, Global.ui_scale)), 128, 256); + m_thread = std::thread(&image_cache::work, this); +} + +image_cache::~image_cache() +{ + { + std::lock_guard lock(m_lock); + m_quit = true; + } + m_wake.notify_all(); + if (m_thread.joinable()) + { + m_thread.join(); + } + // the textures aren't released: the cache goes when the simulator closes, and the gl context may be gone by then +} + +std::uint64_t image_cache::image(std::string const &Path) +{ + auto &entry{m_entries[Path]}; + entry.used = m_frame; + if (entry.texture != 0 || entry.missing || entry.pending) + { + return entry.texture; + } + entry.pending = true; + { + std::lock_guard lock(m_lock); + m_queue.push_front(Path); + } + m_wake.notify_one(); + return 0; +} + +bool image_cache::missing(std::string const &Path) const +{ + auto const lookup{m_entries.find(Path)}; + return lookup != m_entries.end() && lookup->second.missing; +} + +void image_cache::update() +{ + ++m_frame; + std::vector done; + { + std::lock_guard lock(m_lock); + // the queue keeps only what was asked for lately; the rest is asked for again when it's scrolled back into sight + while (m_queue.size() > image_limit / 2) + { + auto const lookup{m_entries.find(m_queue.back())}; + if (lookup != m_entries.end()) + { + lookup->second.pending = false; + } + m_queue.pop_back(); + } + auto const count{std::min(m_done.size(), uploads_per_frame)}; + done.assign(std::make_move_iterator(m_done.begin()), std::make_move_iterator(m_done.begin() + count)); + m_done.erase(m_done.begin(), m_done.begin() + count); + } + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + for (auto &image : done) + { + if (image.generation != m_generation) + { + continue; + } + auto const lookup{m_entries.find(image.path)}; + if (lookup == m_entries.end()) + { + continue; + } + auto &entry{lookup->second}; + entry.pending = false; + if (image.rgba.empty()) + { + entry.missing = true; + continue; + } + entry.texture = (renderer != nullptr ? renderer->Create_Image(image.rgba.data(), image.width, image.height) : 0); + // a renderer which can't show them gets no more requests + entry.missing = (entry.texture == 0); + } + // over the limit, the images not asked for the longest are dropped + std::size_t textures{0}; + for (auto const &entry : m_entries) + { + textures += (entry.second.texture != 0 ? 1 : 0); + } + if (textures <= image_limit) + { + return; + } + std::vector> loaded; + loaded.reserve(textures); + for (auto const &entry : m_entries) + { + if (entry.second.texture != 0) + { + loaded.emplace_back(entry.second.used, &entry.first); + } + } + std::sort(loaded.begin(), loaded.end()); + std::vector dropped; + for (std::size_t index = 0; index < textures - image_limit; ++index) + { + dropped.push_back(*loaded[index].second); + } + for (auto const &path : dropped) + { + auto const lookup{m_entries.find(path)}; + release(lookup->second); + m_entries.erase(lookup); + } +} + +void image_cache::clear() +{ + { + std::lock_guard lock(m_lock); + m_queue.clear(); + m_done.clear(); + ++m_generation; + } + for (auto &entry : m_entries) + { + release(entry.second); + } + m_entries.clear(); +} + +void image_cache::release(entry &Entry) +{ + if (Entry.texture != 0 && GfxRenderer && GfxRenderer->GetImguiRenderer() != nullptr) + { + GfxRenderer->GetImguiRenderer()->Release_Image(Entry.texture); + } + Entry.texture = 0; +} + +void image_cache::work() +{ + // the texture loader turns its images upside down for all threads; these are read as they are + stbi_set_flip_vertically_on_load_thread(0); + while (true) + { + decoded image; + { + std::unique_lock lock(m_lock); + m_wake.wait(lock, [this]() { return m_quit || false == m_queue.empty(); }); + if (m_quit) + { + return; + } + image.path = m_queue.front(); + image.generation = m_generation; + m_queue.pop_front(); + } + int width{0}, height{0}, components{0}; + auto *pixels{stbi_load(image.path.c_str(), &width, &height, &components, 4)}; + if (pixels != nullptr && width > 0 && height > 0) + { + // scaled down by the average of the pixels which make each one, the longer side to the size of the cache + auto const scale{std::max(1.0f, static_cast(std::max(width, height)) / m_size)}; + image.width = std::max(1, static_cast(width / scale)); + image.height = std::max(1, static_cast(height / scale)); + image.rgba.resize(static_cast(image.width) * image.height * 4); + for (int y = 0; y < image.height; ++y) + { + auto const y0{static_cast(y * scale)}; + auto const y1{std::max(y0 + 1, std::min(height, static_cast((y + 1) * scale)))}; + for (int x = 0; x < image.width; ++x) + { + auto const x0{static_cast(x * scale)}; + auto const x1{std::max(x0 + 1, std::min(width, static_cast((x + 1) * scale)))}; + std::uint32_t sum[4]{0, 0, 0, 0}; + for (int sy = y0; sy < y1; ++sy) + { + auto const *row{pixels + (static_cast(sy) * width + x0) * 4}; + for (int sx = x0; sx < x1; ++sx, row += 4) + { + sum[0] += row[0]; + sum[1] += row[1]; + sum[2] += row[2]; + sum[3] += row[3]; + } + } + auto const count{static_cast((y1 - y0) * (x1 - x0))}; + auto *target{image.rgba.data() + (static_cast(y) * image.width + x) * 4}; + for (int channel = 0; channel < 4; ++channel) + { + target[channel] = static_cast(sum[channel] / count); + } + } + } + } + if (pixels != nullptr) + { + stbi_image_free(pixels); + } + { + std::lock_guard lock(m_lock); + m_done.emplace_back(std::move(image)); + } + } +} + +} // namespace editor_previews diff --git a/editor/editorPreviews.hpp b/editor/editorPreviews.hpp new file mode 100644 index 00000000..98c4a5a0 --- /dev/null +++ b/editor/editorPreviews.hpp @@ -0,0 +1,94 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// preview images of the models of the node bank: made by -generate-nodebank-previews, shown by the node bank of the editor. +// both find an image by the same name, made here +namespace editor_previews +{ + +// folder of the images, under the folder of the simulator +extern std::string const folder; + +// characters which can be safely used in a file name, the others replaced by _ +std::string file_name_part(std::string Text); +// path of the image of the node with specified model and skin, without the extension: the path of the model in the preview folder, +// the skin added to the name +std::string preview_path(std::string Model, std::string const &Skin); +// path of the image of a node bank entry ("node ... model ..."), without the extension; +// empty if the entry isn't a model +std::string entry_preview_path(std::string const &Entry); +// name given to another entry with the same model and skin (other lights or angles): ~2, ~3... added in the order of the +// node bank, after the names already Taken +std::string variant(std::string const &Path, std::set const &Taken); + +// images of the node bank as textures of the user interface, small and kept for a while: the files are read and scaled down +// in a thread of their own, and uploaded in the main thread, a few in a frame +class image_cache +{ + public: + image_cache(); + ~image_cache(); + image_cache(image_cache const &) = delete; + image_cache &operator=(image_cache const &) = delete; + + // texture of the image at specified path (with the extension), 0 while it's being read or when there's no such image. + // an image not asked for longer than others is dropped when there are too many + std::uint64_t image(std::string const &Path); + // true if there's no image at specified path + bool missing(std::string const &Path) const; + // to be called once in a frame: uploads the images read since the last frame + void update(); + // drops all images, so that they're read again (e.g. after new ones were generated) + void clear(); + + private: + struct entry + { + std::uint64_t texture{0}; + bool missing{false}; + bool pending{false}; // waits in the queue or is being read + std::uint64_t used{0}; // frame it was asked for in the last time + }; + struct decoded + { + std::string path; + int width{0}; + int height{0}; + std::vector rgba; // empty if there's no such image + unsigned int generation{0}; + }; + void work(); + void release(entry &Entry); + // members + std::unordered_map m_entries; + std::uint64_t m_frame{0}; + int m_size{128}; // pixels of the longer side of the images + // shared with the thread + std::mutex m_lock; + std::condition_variable m_wake; + std::deque m_queue; // the image asked for in the last is read first, it's the one on the screen + std::vector m_done; + unsigned int m_generation{0}; // images read before the last clear() are thrown away + bool m_quit{false}; + std::thread m_thread; +}; + +} // namespace editor_previews diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 99d6a460..de62f90a 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -96,6 +96,10 @@ bool editorSettings::load() parse(value, m_platform_edge); else if (key == "signal_offset") parse(value, m_signal_offset); + else if (key == "nodebank_previews") + m_nodebank_previews = (value == "1"); + else if (key == "nodebank_card") + parse(value, m_nodebank_card); else if (key.rfind(origin_prefix, 0) == 0) { // northing;easting @@ -136,6 +140,8 @@ bool editorSettings::save() stream << "ortho_in_scene " << (m_orthophoto.in_scene ? 1 : 0) << "\n"; stream << "platform_edge " << m_platform_edge << "\n"; stream << "signal_offset " << m_signal_offset << "\n"; + stream << "nodebank_previews " << (m_nodebank_previews ? 1 : 0) << "\n"; + stream << "nodebank_card " << m_nodebank_card << "\n"; stream << std::fixed << std::setprecision(3); for (auto const &origin : m_orthophoto_origins) stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n"; diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index 87be836f..48b150f6 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -44,12 +44,20 @@ public: double signal_offset() const { return m_signal_offset; } void signal_offset(double Distance) { m_signal_offset = Distance; } + // node bank: cards with the previews of the models or a list, and the size of the cards in pixels + bool nodebank_previews() const { return m_nodebank_previews; } + void nodebank_previews(bool Previews) { m_nodebank_previews = Previews; } + int nodebank_card() const { return m_nodebank_card; } + void nodebank_card(int Size) { m_nodebank_card = Size; } + private: movement_scheme m_movement{movement_scheme::wsad}; orthophoto_settings m_orthophoto; std::map> m_orthophoto_origins; double m_platform_edge{1.725}; double m_signal_offset{2.25}; + bool m_nodebank_previews{true}; + int m_nodebank_card{96}; }; // global editor settings instance diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 4c663b45..26e086a9 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -4656,3 +4656,21 @@ msgstr "Odepnij wszystkie doki" msgid "Pin the docks back" msgstr "Przypnij doki z powrotem" + +msgid "Previews" +msgstr "Podglądy" + +msgid "%d px" +msgstr "%d px" + +msgid "No previews yet: they're made by eu07 -generate-nodebank-previews" +msgstr "Brak podglądów: tworzy je eu07 -generate-nodebank-previews" + +msgid "Scenery templates (.inc) are listed with the previews off" +msgstr "Szablony scenerii (.inc) są na liście przy wyłączonych podglądach" + +msgid "not a model" +msgstr "to nie model" + +msgid "no preview" +msgstr "brak podglądu" diff --git a/nodebank_previews_standalone.cpp b/nodebank_previews_standalone.cpp index 8a171689..39884fd3 100644 --- a/nodebank_previews_standalone.cpp +++ b/nodebank_previews_standalone.cpp @@ -7,6 +7,7 @@ #include "utilities/Logs.h" #include "utilities/parser.h" #include "utilities/utilities.h" +#include "editor/editorPreviews.hpp" #include #include @@ -17,8 +18,6 @@ namespace { -std::string const preview_folder{"textures/previews/nodebank/"}; - struct preview_settings { std::vector entries; // node definitions to draw. the whole node bank if empty @@ -183,59 +182,6 @@ GLFWwindow *create_context(bool const Software) return window; } -// characters which can be safely used in a file name -std::string file_name_part(std::string Text) -{ - for (auto &character : Text) - { - auto const c{static_cast(character)}; - if ((false == std::isalnum(c)) && (c != '_') && (c != '-') && (c != '.') && (c != '+') && (c != '(') && (c != ')')) - { - character = '_'; - } - } - if ((Text.empty()) || (Text == ".") || (Text == "..")) - { - Text = "_"; - } - return Text; -} - -// path of the image of the node with specified model and skin: the path of the model in the preview folder, the skin added to the name -std::string preview_path(std::string Model, std::string const &Skin) -{ - replace_slashes(Model); - erase_extension(Model); - std::string path; - std::size_t start{0}; - while (start <= Model.size()) - { - auto end{Model.find('/', start)}; - if (end == std::string::npos) - { - end = Model.size(); - } - if (end > start) - { - if (false == path.empty()) - { - path += '/'; - } - path += file_name_part(Model.substr(start, end - start)); - } - start = end + 1; - } - if ((false == Skin.empty()) && (Skin != "none")) - { - auto skin{Skin}; - replace_slashes(skin); - erase_extension(skin); - std::replace(skin.begin(), skin.end(), '/', '_'); - path += '@' + file_name_part(skin); - } - return preview_folder + path; -} - bool save_png(std::string const &Path, std::vector const &Image, int const Size) { std::error_code error; @@ -295,17 +241,9 @@ bool generate_preview(std::string const &Definition, preview_settings const &Set names.getTokens(9, false); auto const model{names.getToken()}; auto const skin{names.getToken(false)}; - auto path{preview_path(model, skin)}; - if (Saved.contains(path)) - { - // another entry with the same model and skin, but other lights or angles - auto variant{2}; - while (Saved.contains(path + "~" + std::to_string(variant))) - { - ++variant; - } - path += "~" + std::to_string(variant); - } + // another entry with the same model and skin, but other lights or angles, gets a number; the node bank of the editor + // counts the entries the same way to find the image + auto path{editor_previews::variant(editor_previews::preview_path(model, skin), Saved)}; path += ".png"; std::vector image; @@ -446,7 +384,7 @@ int generate_nodebank_previews(int Argc, char *Argv[]) } report("[" + std::to_string(idx + 1) + "/" + std::to_string(entries.size()) + "] " + entries[idx] + (success ? " -> " : ": ") + outcome, (false == success)); } - report(std::to_string(saved.size()) + " previews saved in " + preview_folder + (failures > 0 ? ", " + std::to_string(failures) + " entries failed" : "")); + report(std::to_string(saved.size()) + " previews saved in " + editor_previews::folder + (failures > 0 ? ", " + std::to_string(failures) + " entries failed" : "")); return finish(failures > 0 ? 1 : 0); } diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 3e57c44e..d5d403f8 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -6282,3 +6282,33 @@ void opengl33_renderer::opengl33_imgui_renderer::Render() ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } +std::uint64_t opengl33_renderer::opengl33_imgui_renderer::Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) +{ + // the ui colours are made linear in its shaders, the image is too, by sampling it as srgb. the bindings the renderer + // keeps track of are put back as they were + GLint texture{0}, unpack{0}, alignment{4}; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture); + glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &unpack); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + GLuint image{0}; + glGenTextures(1, &image); + glBindTexture(GL_TEXTURE_2D, image); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, Width, Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, Rgba); + glBindTexture(GL_TEXTURE_2D, static_cast(texture)); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast(unpack)); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); + return image; +} + +void opengl33_renderer::opengl33_imgui_renderer::Release_Image(std::uint64_t const Image) +{ + auto const image{static_cast(Image)}; + glDeleteTextures(1, &image); +} + diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 78684ae4..6d16cb2f 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -579,6 +579,8 @@ class opengl33_renderer : public gfx_renderer { virtual void Shutdown() override; virtual void BeginFrame() override; virtual void Render() override; + virtual std::uint64_t Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) override; + virtual void Release_Image(std::uint64_t const Image) override; } m_imgui_renderer; virtual imgui_renderer* GetImguiRenderer() override { diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 27c94da8..b8a73cfb 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -4565,3 +4565,29 @@ void opengl_renderer::opengl_imgui_renderer::Render() ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); } +std::uint64_t opengl_renderer::opengl_imgui_renderer::Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) +{ + // the ui of this renderer works on srgb colours as they are. the binding the renderer keeps track of is put back as it was + GLint texture{0}, alignment{4}; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + GLuint image{0}; + glGenTextures(1, &image); + glBindTexture(GL_TEXTURE_2D, image); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Width, Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, Rgba); + glBindTexture(GL_TEXTURE_2D, static_cast(texture)); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); + return image; +} + +void opengl_renderer::opengl_imgui_renderer::Release_Image(std::uint64_t const Image) +{ + auto const image{static_cast(Image)}; + glDeleteTextures(1, &image); +} + diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 83dca483..78fffa40 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -385,6 +385,8 @@ private: virtual void Shutdown() override; virtual void BeginFrame() override; virtual void Render() override; + virtual std::uint64_t Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) override; + virtual void Release_Image(std::uint64_t const Image) override; } m_imgui_renderer; virtual imgui_renderer *GetImguiRenderer() override diff --git a/rendering/renderer.h b/rendering/renderer.h index e02959ff..6e8e7d46 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -121,6 +121,13 @@ class imgui_renderer virtual void Shutdown() = 0; virtual void BeginFrame() = 0; virtual void Render() = 0; + // images of the user interface itself (e.g. the previews of the node bank), apart from the textures of the scene: rgba, rows + // from the top, in srgb. returns: id for ImGui::Image, 0 if the renderer can't make them + virtual std::uint64_t Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) + { + return 0; + } + virtual void Release_Image(std::uint64_t const Image) {} }; extern std::unique_ptr GfxRenderer;