17
0
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:
2026-10-08 23:55:56 +02:00
parent 4b451622de
commit 9b0e88db7a
14 changed files with 737 additions and 68 deletions

View File

@@ -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"

View File

@@ -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};

View File

@@ -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
View 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
View 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

View File

@@ -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";

View File

@@ -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

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;