mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-19 06:09:18 +02:00
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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 <filesystem>
|
||||
#include <set>
|
||||
|
||||
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<std::string>(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<std::string>(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<std::string> 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<std::pair<std::string, std::shared_ptr<std::string>> 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<int>((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (cell.x + style.ItemSpacing.x)))};
|
||||
auto const rows{(static_cast<int>(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<std::size_t>(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<ImTextureID>(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<editor_previews::image_cache>();
|
||||
}
|
||||
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<std::pair<std::string, std::shared_ptr<std::string>> 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};
|
||||
|
||||
@@ -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 <functional>
|
||||
/*
|
||||
@@ -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<std::pair<std::string, std::shared_ptr<std::string>> 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<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
|
||||
char m_nodesearch[128];
|
||||
std::shared_ptr<std::string> m_selectedtemplate;
|
||||
// previews of the models: image of each node definition, by its address, and the images read so far
|
||||
std::unordered_map<std::string const *, std::string> m_previewpaths;
|
||||
std::unique_ptr<editor_previews::image_cache> 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
|
||||
|
||||
345
editor/editorPreviews.cpp
Normal file
345
editor/editorPreviews.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
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<unsigned char>(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 <range max> <range min> <name> <type>
|
||||
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<std::string>()};
|
||||
auto const skin{names.getToken<std::string>(false)};
|
||||
if (model.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return preview_path(model, skin);
|
||||
}
|
||||
|
||||
std::string variant(std::string const &Path, std::set<std::string> 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<int>(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<std::mutex> 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<std::mutex> 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<decoded> done;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::size_t>(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<std::pair<std::uint64_t, std::string const *>> 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<std::string> 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<std::mutex> 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<std::mutex> 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<float>(std::max(width, height)) / m_size)};
|
||||
image.width = std::max(1, static_cast<int>(width / scale));
|
||||
image.height = std::max(1, static_cast<int>(height / scale));
|
||||
image.rgba.resize(static_cast<std::size_t>(image.width) * image.height * 4);
|
||||
for (int y = 0; y < image.height; ++y)
|
||||
{
|
||||
auto const y0{static_cast<int>(y * scale)};
|
||||
auto const y1{std::max(y0 + 1, std::min(height, static_cast<int>((y + 1) * scale)))};
|
||||
for (int x = 0; x < image.width; ++x)
|
||||
{
|
||||
auto const x0{static_cast<int>(x * scale)};
|
||||
auto const x1{std::max(x0 + 1, std::min(width, static_cast<int>((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<std::size_t>(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<std::uint32_t>((y1 - y0) * (x1 - x0))};
|
||||
auto *target{image.rgba.data() + (static_cast<std::size_t>(y) * image.width + x) * 4};
|
||||
for (int channel = 0; channel < 4; ++channel)
|
||||
{
|
||||
target[channel] = static_cast<std::uint8_t>(sum[channel] / count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pixels != nullptr)
|
||||
{
|
||||
stbi_image_free(pixels);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_lock);
|
||||
m_done.emplace_back(std::move(image));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace editor_previews
|
||||
94
editor/editorPreviews.hpp
Normal file
94
editor/editorPreviews.hpp
Normal file
@@ -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 <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// 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 <x> <y> <z> <angle> <model> <skin> ..."), 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<std::string> 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<std::uint8_t> rgba; // empty if there's no such image
|
||||
unsigned int generation{0};
|
||||
};
|
||||
void work();
|
||||
void release(entry &Entry);
|
||||
// members
|
||||
std::unordered_map<std::string, entry> 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<std::string> m_queue; // the image asked for in the last is read first, it's the one on the screen
|
||||
std::vector<decoded> 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
|
||||
@@ -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";
|
||||
|
||||
@@ -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<std::string, std::pair<double, double>> 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "utilities/Logs.h"
|
||||
#include "utilities/parser.h"
|
||||
#include "utilities/utilities.h"
|
||||
#include "editor/editorPreviews.hpp"
|
||||
#include <png.h>
|
||||
#include <cstring>
|
||||
|
||||
@@ -17,8 +18,6 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
std::string const preview_folder{"textures/previews/nodebank/"};
|
||||
|
||||
struct preview_settings
|
||||
{
|
||||
std::vector<std::string> 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<unsigned char>(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<std::uint8_t> 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<std::string>()};
|
||||
auto const skin{names.getToken<std::string>(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<std::uint8_t> 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);
|
||||
}
|
||||
|
||||
@@ -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<GLuint>(texture));
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(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<GLuint>(Image)};
|
||||
glDeleteTextures(1, &image);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<GLuint>(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<GLuint>(Image)};
|
||||
glDeleteTextures(1, &image);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<gfx_renderer> GfxRenderer;
|
||||
|
||||
Reference in New Issue
Block a user