17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 05:09:20 +02:00

feat(editor): the node bank in two panes, as the assets of unity: its groups on the left with the scenery templates, the entries of the chosen one on the right, as cards or a list, a search going through all of them; the models dragged from the node bank onto a model set, the templates of a set or the list of the brush or the area fill; the templates of the model sets shown with their previews; the model sets docked by the node bank by default; Polish translation

This commit is contained in:
2026-10-09 12:01:00 +02:00
parent 645a54a739
commit 1bf687b4c9
4 changed files with 379 additions and 130 deletions

View File

@@ -98,8 +98,12 @@ void editor_ui::build_default_layout(unsigned int const Dockspace)
ImGui::DockBuilderDockWindow(window, leftbottom);
for (auto const *window : {"###inspector", "###editorsettings", "###Include database", "###orthophoto", "###environment"})
ImGui::DockBuilderDockWindow(window, right);
for (auto const *window : {"###Node bank", "###profilestrip", "###trackanalysis", "###structuregauge", "###editorhistory", "###modelsets"})
ImGui::DockBuilderDockWindow(window, bottom);
// bottom: the node bank, and the model sets by its side, the models are dragged from the one to the other
ImGuiID bottomleft{bottom};
auto const bottomright{ImGui::DockBuilderSplitNode(bottomleft, ImGuiDir_Right, 0.45f, nullptr, &bottomleft)};
for (auto const *window : {"###Node bank", "###profilestrip", "###trackanalysis", "###structuregauge", "###editorhistory"})
ImGui::DockBuilderDockWindow(window, bottomleft);
ImGui::DockBuilderDockWindow("###modelsets", bottomright);
ImGui::DockBuilderFinish(Dockspace);
}

View File

@@ -59,6 +59,71 @@ bool list_item(char const *Label, bool const Selected)
ImGui::PopStyleColor();
return clicked;
}
// name of the payload of a node definition dragged out of the node bank
char const *const template_payload{"EU07_NODEBANK_ENTRY"};
// a node definition of the node bank dropped on the item drawn last. returns: true if one was, with its text
bool dropped_template(std::string &Template)
{
auto dropped{false};
if (ImGui::BeginDragDropTarget())
{
if (auto const *payload{ImGui::AcceptDragDropPayload(template_payload)}; payload != nullptr && payload->DataSize > 0)
{
Template.assign(static_cast<char const *>(payload->Data), static_cast<std::size_t>(payload->DataSize));
dropped = true;
}
ImGui::EndDragDropTarget();
}
return dropped;
}
// a card: the image fills its square, or what's missing is said in its place, and the label is under it, cut short with an
// ellipsis. returns: true if it was clicked; the card is the item drawn last
bool card(ImVec2 const &Cell, float const Side, std::uint64_t const Texture, char const *Placeholder, char const *Label, bool const Selected)
{
auto const &style{ImGui::GetStyle()};
auto const position{ImGui::GetCursorScreenPos()};
auto const clicked{ImGui::InvisibleButton("##card", Cell)};
auto const hovered{ImGui::IsItemHovered()};
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);
ImVec2 const imagemin{position.x + 2.0f, position.y + 2.0f};
ImVec2 const imagemax{position.x + Side - 2.0f, position.y + Side - 2.0f};
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();
}
while (*Label == ' ')
{
++Label;
}
ImVec2 const labelmin{position.x + style.FramePadding.x, position.y + Side + 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);
return clicked;
}
// cells of the cards of the side, and how many of them fit the width in a row
ImVec2 card_cell(float const Side)
{
return ImVec2(Side, Side + ImGui::GetTextLineHeight() + ImGui::GetStyle().FramePadding.y * 2.0f);
}
int card_columns(ImVec2 const &Cell)
{
auto const &style{ImGui::GetStyle()};
return std::max(1, static_cast<int>((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (Cell.x + style.ItemSpacing.x)));
}
} // namespace
void itemproperties_panel::update(scene::basic_node *Node)
@@ -602,6 +667,7 @@ void nodebank_panel::nodebank_reload()
void nodebank_panel::index_previews()
{
m_previewpaths.clear();
m_templatepreviews.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
@@ -620,12 +686,14 @@ void nodebank_panel::index_previews()
auto const name{editor_previews::variant(path, taken)};
taken.insert(name);
m_previewpaths[entry.second.get()] = name + ".png";
m_templatepreviews.try_emplace(*entry.second, name + ".png");
}
}
void nodebank_panel::preview_popup(std::pair<std::string, std::shared_ptr<std::string>> const &Entry)
{
if (false == ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay))
// not while something's dragged, the tooltip of the drag is there then
if (false == ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay) || ImGui::GetDragDropPayload() != nullptr)
{
return;
}
@@ -669,13 +737,62 @@ void nodebank_panel::preview_popup(std::pair<std::string, std::shared_ptr<std::s
ImGui::EndTooltip();
}
std::uint64_t nodebank_panel::preview_texture(std::string const &Path, char const *&Placeholder)
{
if (Path.empty() || m_previews == nullptr)
{
Placeholder = STR_C("not a model");
return 0;
}
auto const texture{m_previews->image(Path)};
Placeholder = (m_previews->missing(Path) ? STR_C("no preview") : "...");
return texture;
}
std::string const &nodebank_panel::template_preview(std::string const &Template) const
{
static std::string const none;
auto const lookup{m_templatepreviews.find(Template)};
return lookup != m_templatepreviews.end() ? lookup->second : none;
}
void nodebank_panel::drag_entry(std::pair<std::string, std::shared_ptr<std::string>> const &Entry)
{
// the scenery templates are placed with include directives, the lists of the models have no use for them
if (Entry.second->empty() || Entry.second->starts_with(editor_includes::directive_mark))
{
return;
}
if (false == ImGui::BeginDragDropSource())
{
return;
}
ImGui::SetDragDropPayload(template_payload, Entry.second->data(), Entry.second->size());
auto const lookup{m_previewpaths.find(Entry.second.get())};
if (lookup != m_previewpaths.end() && m_previews != nullptr)
{
if (auto const texture{m_previews->image(lookup->second)}; texture != 0)
{
auto const side{64.0f * std::max(1.0f, Global.ui_scale)};
ImGui::Image(static_cast<ImTextureID>(texture), ImVec2(side, side));
}
}
auto const *label{Entry.first.c_str()};
while (*label == ' ')
{
++label;
}
ImGui::TextUnformatted(label);
ImGui::TextDisabled("%s", STR_C("Drop it on a model set, or on the list of the brush or the area fill"));
ImGui::EndDragDropSource();
}
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 side{std::max(16.0f, EditorSettings.nodebank_card() * Global.ui_scale)};
auto const cell{card_cell(side)};
auto const columns{card_columns(cell)};
auto const rows{(static_cast<int>(Entries.size()) + columns - 1) / columns};
ImGuiListClipper clipper;
clipper.Begin(rows, cell.y + style.ItemSpacing.y);
@@ -696,49 +813,15 @@ void nodebank_panel::render_cards(std::vector<std::pair<std::string, std::shared
ImGui::SameLine();
}
ImGui::PushID(entry.second.get());
auto const position{ImGui::GetCursorScreenPos()};
if (ImGui::InvisibleButton("##card", cell))
auto const lookup{m_previewpaths.find(entry.second.get())};
char const *placeholder{nullptr};
auto const texture{preview_texture(lookup != m_previewpaths.end() ? lookup->second : std::string{}, placeholder)};
if (card(cell, side, texture, placeholder, entry.first.c_str(), entry.second == m_selectedtemplate))
{
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)
drag_entry(entry);
if (ImGui::IsItemHovered())
{
preview_popup(entry);
}
@@ -749,6 +832,87 @@ void nodebank_panel::render_cards(std::vector<std::pair<std::string, std::shared
clipper.End();
}
void nodebank_panel::render_list(std::vector<std::pair<std::string, std::shared_ptr<std::string>> const *> const &Entries)
{
for (auto const *entry : Entries)
{
ImGui::PushID(entry->second.get());
auto const label{" " + entry->first};
if (list_item(label.c_str(), entry->second == m_selectedtemplate))
{
m_selectedtemplate = entry->second;
}
drag_entry(*entry);
preview_popup(*entry);
ImGui::PopID();
}
}
void nodebank_panel::render_templates_list(std::string const &Filter)
{
if (false == EditorIncludes.scanned())
{
EditorIncludes.scan();
}
auto const &templates{EditorIncludes.entries()};
std::string const *category{nullptr};
auto listed{0};
for (auto const entryidx : EditorIncludes.ready())
{
auto const &entry{templates[entryidx]};
if (false == Filter.empty() && false == contains(entry.name, Filter) && false == contains(entry.file, Filter) && false == contains(entry.category, Filter))
{
continue;
}
if (false == entry.category.empty() && (category == nullptr || *category != entry.category))
{
ImGui::TextDisabled(" %s", entry.category.c_str());
}
category = &entry.category;
auto const label{(entry.category.empty() ? " " : " ") + entry.name + " (" + Bezogonkow(entry.file) + ")##inc" + std::to_string(entryidx)};
if (list_item(label.c_str(), entry.statement == m_selectedtemplate))
m_selectedtemplate = entry.statement;
++listed;
}
if (listed == 0)
{
ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)");
}
}
void nodebank_panel::render_set_cards(int const Setid)
{
auto const *set{EditorModelSets.find(Setid)};
if (set == nullptr)
{
return;
}
// a bit smaller than in the node bank, the window of the sets is the smaller one
auto const side{std::max(48.0f, EditorSettings.nodebank_card() * Global.ui_scale * 0.75f)};
auto const cell{card_cell(side)};
auto const columns{card_columns(cell)};
for (int index = 0; index < static_cast<int>(set->templates.size()); ++index)
{
if (index % columns != 0)
{
ImGui::SameLine();
}
ImGui::PushID(index);
char const *placeholder{nullptr};
auto const texture{preview_texture(template_preview(set->templates[index]), placeholder)};
auto const &label{index < static_cast<int>(set->labels.size()) ? set->labels[index] : set->templates[index]};
if (card(cell, side, texture, placeholder, label.c_str(), index == m_setsentry))
{
m_setsentry = index;
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", label.c_str());
}
ImGui::PopID();
}
}
void nodebank_panel::render()
{
if (false == is_open)
@@ -829,101 +993,132 @@ void nodebank_panel::render()
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))
// two panes: the groups of the node bank on the left, like folders, and the entries of the chosen one on the right.
// a search goes through all of them
struct category
{
// 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)
std::string const *name;
std::vector<std::pair<std::string, std::shared_ptr<std::string>> const *> entries;
int matches{0};
};
static std::string const ungrouped{"(ungrouped)"};
auto const searchfilter{std::string(m_nodesearch)};
std::vector<category> categories;
for (auto const &entry : m_nodebank)
{
if (entry.second->empty())
{
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);
}
categories.push_back({&entry.first, {}});
continue;
}
if (false == cards.empty())
if (categories.empty())
{
render_cards(cards);
// templates listed before the first header, as group_names() has them
categories.push_back({&ungrouped, {}});
}
auto &group{categories.back()};
group.entries.push_back(&entry);
if (false == searchfilter.empty() && contains(entry.first, searchfilter))
{
++group.matches;
}
ImGui::Separator();
ImGui::TextDisabled("%s", STR_C("Scenery templates (.inc) are listed with the previews off"));
}
if (previews)
auto const templates{Global.editor_session};
if (m_category >= static_cast<int>(categories.size()) || (m_category == scenery_templates && false == templates) || m_category < scenery_templates)
{
m_category = 0;
}
// the panes of the height of the table, inside the padding of its cells
auto const paneheight{listheight - 2.0f * ImGui::GetStyle().CellPadding.y - 1.0f};
if (ImGui::BeginTable("##nodebankpanes", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV, ImVec2(-1, listheight)))
{
ImGui::TableSetupColumn("##groups", ImGuiTableColumnFlags_WidthFixed, 170.0f * Global.ui_scale);
ImGui::TableSetupColumn("##entries", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
if (ImGui::BeginChild("##groups", ImVec2(0, paneheight)))
{
auto const searching{false == searchfilter.empty()};
for (int index = 0; index < static_cast<int>(categories.size()); ++index)
{
auto const &group{categories[index]};
auto const count{searching ? group.matches : static_cast<int>(group.entries.size())};
auto const label{*group.name + " (" + std::to_string(count) + ")##group" + std::to_string(index)};
if (searching && count == 0)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
// a group chosen while searching is shown whole, the search is done
if (list_item(label.c_str(), false == searching && index == m_category))
{
m_category = index;
m_nodesearch[0] = '\0';
}
if (searching && count == 0)
{
ImGui::PopStyleColor();
}
}
if (templates)
{
ImGui::Separator();
if (list_item(STR_C("Scenery templates (.inc)"), m_category == scenery_templates))
{
m_category = scenery_templates;
}
}
}
ImGui::EndChild();
}
else if (ImGui::BeginListBox("##nodebank", ImVec2(-1, listheight)))
{
auto idx{0};
auto isvisible{false};
auto const searchfilter{std::string(m_nodesearch)};
for (auto const &entry : m_nodebank)
ImGui::TableSetColumnIndex(1);
if (ImGui::BeginChild("##entries", ImVec2(0, paneheight)))
{
if (entry.second->empty())
if (m_category == scenery_templates)
{
// special case, header indicator
isvisible = ImGui::CollapsingHeader(entry.first.c_str());
render_templates_list(searchfilter);
}
else
else if (false == searchfilter.empty())
{
if (false == isvisible)
// what was found, under the names of the groups
std::vector<std::pair<std::string, std::shared_ptr<std::string>> const *> found;
for (auto const &group : categories)
{
continue;
if (group.matches == 0)
{
continue;
}
found.clear();
for (auto const *entry : group.entries)
{
if (contains(entry->first, searchfilter))
{
found.push_back(entry);
}
}
ImGui::SeparatorText(group.name->c_str());
if (previews)
{
render_cards(found);
}
else
{
render_list(found);
}
}
if (false == searchfilter.empty() && false == contains(entry.first, searchfilter))
}
else if (false == categories.empty())
{
if (previews)
{
continue;
render_cards(categories[m_category].entries);
}
else
{
render_list(categories[m_category].entries);
}
auto const label{" " + entry.first + "##" + std::to_string(idx)};
if (list_item(label.c_str(), entry.second == m_selectedtemplate))
m_selectedtemplate = entry.second;
preview_popup(entry);
++idx;
}
}
// scenery templates. placing one adds an include directive to a scenery file, so they take scenery opened for editing
if (Global.editor_session && ImGui::CollapsingHeader(STR_C("Scenery templates (.inc)")))
{
if (false == EditorIncludes.scanned())
{
EditorIncludes.scan();
}
auto const &templates{EditorIncludes.entries()};
std::string const *category{nullptr};
auto listed{0};
for (auto const entryidx : EditorIncludes.ready())
{
auto const &entry{templates[entryidx]};
if (false == searchfilter.empty() && false == contains(entry.name, searchfilter) && false == contains(entry.file, searchfilter) && false == contains(entry.category, searchfilter))
{
continue;
}
if (false == entry.category.empty() && (category == nullptr || *category != entry.category))
{
ImGui::TextDisabled(" %s", entry.category.c_str());
}
category = &entry.category;
auto const label{(entry.category.empty() ? " " : " ") + entry.name + " (" + Bezogonkow(entry.file) + ")##inc" + std::to_string(entryidx)};
if (list_item(label.c_str(), entry.statement == m_selectedtemplate))
m_selectedtemplate = entry.statement;
++listed;
}
if (listed == 0)
{
ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)");
}
}
ImGui::EndListBox();
ImGui::EndChild();
ImGui::EndTable();
}
ImGui::PopItemWidth();
}
@@ -999,6 +1194,11 @@ void nodebank_panel::manual_list(char const *Id, std::vector<std::string> &List,
}
ImGui::EndListBox();
}
// a model dragged from the node bank onto the list goes to its end
if (std::string dropped; dropped_template(dropped))
{
List.push_back(dropped);
}
if (ImGui::Button(STR_C("Add selected")))
{
if (node_selected())
@@ -1121,7 +1321,8 @@ void nodebank_panel::render_sets_window()
// left side: the sets
auto const buttonsheight{ImGui::GetFrameHeightWithSpacing()};
ImGui::BeginGroup();
ImGui::BeginChild("##setlist", ImVec2(190 * Global.ui_scale, -buttonsheight), true);
// the list of the sets takes up to a part of a narrow window, the templates of the set need the room
ImGui::BeginChild("##setlist", ImVec2(std::min(190.0f * Global.ui_scale, ImGui::GetContentRegionAvail().x * 0.4f), -buttonsheight), true);
for (auto const &set : sets)
{
auto const label{set.name + " (" + std::to_string(set.templates.size()) + ")##set" + std::to_string(set.id)};
@@ -1130,6 +1331,13 @@ void nodebank_panel::render_sets_window()
m_setsselected = set.id;
m_setsentry = -1;
}
// a model dragged from the node bank onto a set goes to its end
if (std::string dropped; dropped_template(dropped))
{
EditorModelSets.add(set.id, {dropped});
m_setsselected = set.id;
m_setsentry = -1;
}
}
if (sets.empty())
{
@@ -1201,17 +1409,30 @@ void nodebank_panel::render_sets_window()
}
ImGui::BeginChild("##setentries", ImVec2(0, -buttonsheight * 2), true);
for (int idx = 0; idx < static_cast<int>(edited->labels.size()); ++idx)
// with the previews of the models, as in the node bank
if (EditorSettings.nodebank_previews() && m_previews != nullptr)
{
auto const label{edited->labels[idx] + "##entry" + std::to_string(idx)};
if (list_item(label.c_str(), idx == m_setsentry))
m_setsentry = idx;
render_set_cards(edited->id);
}
else
{
for (int idx = 0; idx < static_cast<int>(edited->labels.size()); ++idx)
{
auto const label{edited->labels[idx] + "##entry" + std::to_string(idx)};
if (list_item(label.c_str(), idx == m_setsentry))
m_setsentry = idx;
}
}
if (edited->templates.empty())
{
ImGui::TextDisabled(STR_C("(empty) select a template in the node bank and press \"Add selected\""));
ImGui::TextDisabled("%s", STR_C("or drag models here from the node bank"));
}
ImGui::EndChild();
if (std::string dropped; dropped_template(dropped))
{
EditorModelSets.add(edited->id, {dropped});
}
auto const setid{edited->id};
if (ImGui::Button(STR_C("Add selected")))

View File

@@ -166,6 +166,18 @@ class nodebank_panel : public ui_panel
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);
// the same entries as a list, for when the previews are off
void render_list(std::vector<std::pair<std::string, std::shared_ptr<std::string>> const *> const &Entries);
// the scenery templates (.inc), as a list; they're placed with include directives, so they take a scenery opened for editing
void render_templates_list(std::string const &Filter);
// the entry dragged from the item drawn last, to drop it on a list of the model sets or of a tool
void drag_entry(std::pair<std::string, std::shared_ptr<std::string>> const &Entry);
// image of a node definition as the node bank has it, or an empty string
std::string const &template_preview(std::string const &Template) const;
// texture of the small preview of the image, 0 if there's none (yet)
std::uint64_t preview_texture(std::string const &Path, char const *&Placeholder);
// the templates of the edited model set as cards with their previews
void render_set_cards(int const Setid);
// the entry pointed at for a moment: a popup with its preview, big, and the model and skin it shows
void preview_popup(std::pair<std::string, std::shared_ptr<std::string>> const &Entry);
// true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take
@@ -179,6 +191,10 @@ class nodebank_panel : public ui_panel
std::unique_ptr<editor_previews::image_cache> m_previews;
std::unique_ptr<editor_previews::image_cache> m_largepreviews; // a few, for the popup
bool m_previewfolder{false}; // the generator was run, there's the folder of the images
std::unordered_map<std::string, std::string> m_templatepreviews; // images of the node definitions by their text, for the model sets
// the group of the node bank shown, by its place among the groups; scenery_templates for the scenery templates
static int constexpr scenery_templates{-1};
int m_category{0};
// set manager window
bool m_setsopen{false};
int m_setsselected{0}; // id of the user set being edited