17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 12:09:19 +02:00
Files
maszyna/application/editoruipanels.cpp
Hirek193 8cd0bab27b Editor terrain: texture browser, undo, conversion from terrain files
- Terrain work mode shows a browser of the textures folder in place of the
  node bank; textures/materials can be dragged onto the palette entries
  (replacing the material), the material field and the water material field
- Ctrl+Z/Ctrl+Y for heightmap terrain changes: a brush stroke or an
  operation is one step of the editor history
- Ground conversion reads only the files of the terrain directives
  (.txtf with origin/rotate, or .btf without text), paints the chunks with
  the materials of the triangles, drops only the shapes of those files
- Fix: ground grids converted out-of-range floats to int, so whole-scene
  queries returned the triangles of a single cell (shifted chunks)
- Fix: terrain layers copied from textures already uploaded without their
  data stayed grey
- Fix: ImGui ID conflict between terrain tool radios and section headers
- Fix: layers of terrain .txtf files named with the scenery/ prefix and
  could not be saved
- Editor selftest commands for the terrain
2026-10-09 23:39:26 +02:00

2420 lines
75 KiB
C++

/*
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 "application/editoruipanels.h"
#include "scene/scenenodegroups.h"
#include "scene/scenelayers.h"
#include "utilities/Globals.h"
#include "vehicle/Camera.h"
#include "model/AnimModel.h"
#include "world/Track.h"
#include "world/Event.h"
#include "world/MemCell.h"
#include "application/editoruilayer.h"
#include "rendering/renderer.h"
#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
{
// tells why provided text can't be the name of a node, empty text if it can. the scenery parser has to get it back as a single token
std::string node_name_issue(std::string const &Name)
{
if (Name.find_first_of(" \t\r\n;\"") != std::string::npos)
{
return "a name can't hold spaces, semicolons or quotes";
}
if (Name.find("//") != std::string::npos || Name.find("/*") != std::string::npos)
{
return "a name can't hold a comment mark";
}
if (Name == "include")
{
return "this word has a meaning of its own in scenery files";
}
return {};
}
// list entry; the selected one is drawn with the accent colour, like selections in the starter
bool list_item(char const *Label, bool const Selected)
{
if (Selected)
ImGui::PushStyleColor(ImGuiCol_Header, ImGui::GetStyleColorVec4(ImGuiCol_HeaderActive));
auto const clicked{ImGui::Selectable(Label, Selected)};
if (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)
{
m_node = Node;
if (false == is_open)
{
return;
}
text_lines.clear();
m_grouplines.clear();
std::string textline;
// scenario inspector
auto const *node{Node};
auto const &camera{Global.pCamera};
if (node == nullptr)
{
auto const mouseposition{camera.Pos + GfxRenderer->Mouse_Position()};
textline = "mouse location: [" + to_string(mouseposition.x, 2) + ", " + to_string(mouseposition.y, 2) + ", " + to_string(mouseposition.z, 2) + "]";
text_lines.emplace_back(textline, Global.UITextColor);
return;
}
/*
// TODO: bind receiver in the constructor
if( ( m_itemproperties != nullptr )
&& ( m_itemproperties->node != nullptr ) ) {
// fetch node data; skip properties which were changed until they're retrieved by the observer
auto const *node { m_itemproperties->node };
if( m_itemproperties->name.second == false ) {
m_itemproperties->name.first = ( node->name().empty() ? "(none)" : node->name() );
}
if( m_itemproperties->location.second == false ) {
m_itemproperties->location.first = node->location();
}
}
*/
textline = "name: " + (node->name().empty() ? "(none)" : Bezogonkow(node->name())) + "\ntype: " + node->node_type + "\nlocation: [" + to_string(node->location().x, 2) + ", " + to_string(node->location().y, 2) + ", " +
to_string(node->location().z, 2) + "]" +
" (distance: " + to_string(glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z}), 1) + " m)" + "\nUUID: " + node->uuid.to_string();
if (scene::Layers.valid(node->layer()))
{
textline += "\nlayer: " + Bezogonkow(scene::Layers.layer(node->layer()).name);
}
text_lines.emplace_back(textline, Global.UITextColor);
// subclass-specific data
// TBD, TODO: specialized data dump method in each node subclass, or data imports in the panel for provided subclass pointer?
if (typeid(*node) == typeid(TAnimModel))
{
auto const *subnode = static_cast<TAnimModel const *>(node);
textline = "angle_x: " + to_string(clamp_circular(subnode->vAngle.x, 360.f), 2) + " deg, " + "angle_y: " + to_string(clamp_circular(subnode->vAngle.y, 360.f), 2) + " deg, " +
"angle_z: " + to_string(clamp_circular(subnode->vAngle.z, 360.f), 2) + " deg";
textline += ";\nlights: ";
if (subnode->iNumLights > 0)
{
textline += '[';
for (int lightidx = 0; lightidx < subnode->iNumLights; ++lightidx)
{
textline += std::to_string(subnode->lsLights[lightidx]);
if (lightidx < subnode->iNumLights - 1)
{
textline += ", ";
}
}
textline += ']';
}
else
{
textline += "(none)";
}
text_lines.emplace_back(textline, Global.UITextColor);
// 3d shape
auto modelfile{(subnode->pModel != nullptr ? subnode->pModel->NameGet() : "(none)")};
if (modelfile.find(paths::models) == 0)
{
// don't include 'models/' in the path
modelfile.erase(0, std::string{paths::models}.size());
}
// texture
auto texturefile{(subnode->Material()->replacable_skins[1] != null_handle ? GfxRenderer->Material(subnode->Material()->replacable_skins[1])->GetName() : "(none)")};
if (texturefile.find(paths::textures) == 0)
{
// don't include 'textures/' in the path
texturefile.erase(0, std::string{paths::textures}.size());
}
text_lines.emplace_back("mesh: " + modelfile, Global.UITextColor);
text_lines.emplace_back("skin: " + texturefile, Global.UITextColor);
}
else if (typeid(*node) == typeid(TTrack))
{
auto const *subnode = static_cast<TTrack const *>(node);
std::string isolatedlist;
for (const TIsolated *iso : subnode->Isolated)
{
if (!isolatedlist.empty())
isolatedlist += ", ";
isolatedlist += iso->asName;
}
// basic attributes
textline = "isolated: " + (!isolatedlist.empty() ? isolatedlist : "(none)") + "\nvelocity: " + std::to_string(subnode->SwitchExtension ? subnode->SwitchExtension->fVelocity : subnode->fVelocity) +
"\nwidth: " + std::to_string(subnode->fTrackWidth) + " m" + "\nfriction: " + to_string(subnode->fFriction, 2) + "\nquality: " + std::to_string(subnode->iQualityFlag);
text_lines.emplace_back(textline, Global.UITextColor);
// textures
auto texturefile{(subnode->m_material1 != null_handle ? GfxRenderer->Material(subnode->m_material1)->GetName() : "(none)")};
if (texturefile.find(paths::textures) == 0)
{
texturefile.erase(0, std::string{paths::textures}.size());
}
auto texturefile2{(subnode->m_material2 != null_handle ? GfxRenderer->Material(subnode->m_material2)->GetName() : "(none)")};
if (texturefile2.find(paths::textures) == 0)
{
texturefile2.erase(0, std::string{paths::textures}.size());
}
textline = "skins:\n " + texturefile + "\n " + texturefile2;
text_lines.emplace_back(textline, Global.UITextColor);
// paths
textline = "paths: ";
for (auto const &path : subnode->m_paths)
{
textline += "\n [" + to_string(path.points[segment_data::point::start].x, 3) + ", " + to_string(path.points[segment_data::point::start].y, 3) + ", " +
to_string(path.points[segment_data::point::start].z, 3) + "]->" + " [" + to_string(path.points[segment_data::point::end].x, 3) + ", " +
to_string(path.points[segment_data::point::end].y, 3) + ", " + to_string(path.points[segment_data::point::end].z, 3) + "] ";
}
text_lines.emplace_back(textline, Global.UITextColor);
// events
textline.clear();
std::vector<std::pair<std::string, TTrack::event_sequence const *>> const eventsequences{{"ev0", &subnode->m_events0}, {"ev0all", &subnode->m_events0all},
{"ev1", &subnode->m_events1}, {"ev1all", &subnode->m_events1all},
{"ev2", &subnode->m_events2}, {"ev2all", &subnode->m_events2all}};
for (auto const &eventsequence : eventsequences)
{
if (eventsequence.second->empty())
{
continue;
}
textline += (textline.empty() ? "" : "\n") + eventsequence.first + ": [";
for (auto const &event : *eventsequence.second)
{
if (textline.back() != '[')
{
textline += ", ";
}
textline += event.second != nullptr ? Bezogonkow(event.second->m_name) : event.first + " (missing)";
}
textline += "] ";
}
text_lines.emplace_back(textline, Global.UITextColor);
}
else if (typeid(*node) == typeid(TMemCell))
{
auto const *subnode = static_cast<TMemCell const *>(node);
textline = "data: [" + subnode->Text() + "]" + " [" + to_string(subnode->Value1(), 2) + "]" + " [" + to_string(subnode->Value2(), 2) + "]";
text_lines.emplace_back(textline, Global.UITextColor);
textline = "track: " + (subnode->asTrackName.empty() ? "(none)" : Bezogonkow(subnode->asTrackName));
text_lines.emplace_back(textline, Global.UITextColor);
}
update_group();
}
void itemproperties_panel::update_group()
{
auto const grouphandle{m_node->group()};
if (grouphandle == null_handle)
{
m_grouphandle = null_handle;
m_groupprefix.clear();
return;
}
auto const &nodegroup{scene::Groups.group(grouphandle)};
if (m_grouphandle != grouphandle)
{
// calculate group name from shared prefix of item names
std::vector<std::reference_wrapper<std::string const>> names;
// build list of custom item and event names
for (auto const *node : nodegroup.nodes)
{
auto const &name{node->name()};
if (name.empty() || name == "none")
{
continue;
}
names.emplace_back(name);
}
for (auto const *event : nodegroup.events)
{
auto const &name{event->m_name};
if (name.empty() || name == "none")
{
continue;
}
names.emplace_back(name);
}
// find the common prefix
if (names.size() > 1)
{
m_groupprefix = names.front();
for (auto const &name : names)
{
// NOTE: first calculation runs over two instances of the same name, but, eh
auto const prefixlength{len_common_prefix(m_groupprefix, name.get())};
if (prefixlength > 0)
{
m_groupprefix = m_groupprefix.substr(0, prefixlength);
}
else
{
m_groupprefix.clear();
break;
}
}
}
else
{
// less than two names to compare means no prefix
m_groupprefix.clear();
}
m_grouphandle = grouphandle;
}
m_grouplines.emplace_back("nodes: " + std::to_string(nodegroup.nodes.size()) + "\nevents: " + std::to_string(nodegroup.events.size()), Global.UITextColor);
m_grouplines.emplace_back("names prefix: " + (m_groupprefix.empty() ? "(none)" : m_groupprefix), Global.UITextColor);
}
void itemproperties_panel::render()
{
if (false == is_open)
{
return;
}
if (true == text_lines.empty())
{
return;
}
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
if (size.x > 0)
{
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
}
if (size_min.x > 0)
{
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2(size_max.x, size_max.y));
}
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{
render_body();
}
ImGui::End();
}
void itemproperties_panel::render_body()
{
if (m_include != nullptr)
{
render_include();
return;
}
// header section
for (auto const &line : text_lines)
{
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
}
// name of the node — TAnimModel only
render_name_editor();
// transform editor (position/rotation/scale) — TAnimModel only
render_transform_editor();
// group section
render_group();
}
// Name of a picked TAnimModel. The editor places decorations without names, as each name takes its share of the time
// the scenery needs to load; this is where an instance gets one, for the events to refer to it.
void itemproperties_panel::render_name_editor()
{
if (m_node == nullptr) { return; }
if (typeid(*m_node) != typeid(TAnimModel)) { return; }
auto *picked = static_cast<TAnimModel *>(m_node);
// the name of an instance defined by a template, or by a file which can't be rewritten, wouldn't make it to the scenery files
if (false == scene::Layers.editable(picked)) { return; }
if (m_namednode != m_node || false == m_nameactive)
{
// the field shows the name of the node unless it's being typed in
if (m_namednode != m_node)
{
m_nameissue.clear();
}
m_namednode = m_node;
std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str());
}
auto const entered{ImGui::InputTextWithHint(STR_C("name"), STR_C("(none)"), m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)};
m_nameactive = ImGui::IsItemActive();
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it"));
}
if (entered || ImGui::IsItemDeactivatedAfterEdit())
{
// names are lower case, like everything else the scenery parser hands over
std::string name{m_namebuffer};
for (auto &character : name)
{
if (character >= 'A' && character <= 'Z')
{
character = static_cast<char>(character - 'A' + 'a');
}
}
if (name == "none")
{
name.clear();
}
if (name != picked->name())
{
m_nameissue = node_name_issue(name);
if (m_nameissue.empty() && false == simulation::State.rename_model(picked, name))
{
m_nameissue = "another model instance goes by this name";
}
}
else
{
m_nameissue.clear();
}
m_nameactive = false;
}
if (false == m_nameissue.empty())
{
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", m_nameissue.c_str());
}
}
// In-place editor for position (double precision), rotation (degrees, 0-360),
// and uniform scale (per-axis float, 1.000) of a picked TAnimModel.
// Other node subclasses don't expose these knobs through the same API, so the
// editor short-circuits when the bound node isn't a TAnimModel.
void itemproperties_panel::render_transform_editor()
{
if (m_node == nullptr) { return; }
if (typeid(*m_node) != typeid(TAnimModel)) { return; }
auto *picked = static_cast<TAnimModel *>(m_node);
if (false == ImGui::CollapsingHeader(STR_C("Transform"), ImGuiTreeNodeFlags_DefaultOpen))
{
return;
}
// Position — full double precision via DragScalarN. World coordinates can grow
// large; %.6f gives sub-millimetre resolution at typical scenario distances.
{
glm::dvec3 location = picked->location();
double pos[3] = { location.x, location.y, location.z };
if (ImGui::DragScalarN("position", ImGuiDataType_Double, pos, 3, 0.05f, nullptr, nullptr, "%.6f"))
{
picked->location(glm::dvec3(pos[0], pos[1], pos[2]));
}
}
// Rotation — wrapped into [0,360) for display; slider clamps drags to that range.
{
glm::vec3 angles{
clamp_circular(picked->Angles().x),
clamp_circular(picked->Angles().y),
clamp_circular(picked->Angles().z)};
if (ImGui::DragFloat3(STR_C("rotation (deg)"), &angles.x, 0.5f, 0.0f, 360.0f, "%.3f"))
{
picked->Angles(angles);
}
}
// Scale — per-axis float, 1.000 display. Clamped to a reasonable positive range.
{
glm::vec3 scale = picked->Scale();
if (ImGui::DragFloat3(STR_C("scale (x,y,z)"), &scale.x, 0.01f, 0.001f, 100.0f, "%.3f"))
{
picked->Scale(scale);
}
}
if (ImGui::Button(STR_C("reset rotation"))) { picked->Angles(glm::vec3(0.0f)); }
ImGui::SameLine();
if (ImGui::Button(STR_C("reset scale"))) { picked->Scale(glm::vec3(1.0f)); }
}
bool itemproperties_panel::render_group()
{
if (m_node == nullptr)
{
return false;
}
if (m_grouplines.empty())
{
return false;
}
if (false == ImGui::CollapsingHeader(STR_C("Parent Group")))
{
return false;
}
for (auto const &line : m_grouplines)
{
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
}
return true;
}
std::string *brush_object_list::GetRandomObject()
{
static std::string empty; // fallback
if (Objects.empty())
return &empty;
std::uniform_int_distribution<size_t> dist(0, Objects.size() - 1);
return &Objects[dist(Global.local_random_engine)];
}
void brush_object_list::render_options(nodebank_panel &Bank)
{
ImGui::SliderFloat(STR_C("Spacing"), &spacing, 0.1f, 20.0f, "%.1f m");
ImGui::Checkbox(STR_C("Random model from set"), &useRandom);
if (false == useRandom)
{
ImGui::TextDisabled(STR_C("Paints the template selected in the node bank"));
return;
}
Bank.set_combo(STR_C("Preset"), source, STR_C("Manual list"));
if (source.kind == model_set_ref::source::manual)
{
Bank.manual_list("brushset", Objects, idx);
}
else
{
auto const count{Bank.set_entries(source).size()};
ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : "");
}
}
nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen)
{
size_min = {100, 50};
size_max = {1000, 1000};
memset(m_nodesearch, 0, sizeof(m_nodesearch));
std::ifstream file;
file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary);
std::string line;
while (std::getline(file, line))
{
if (line.size() < 4)
{
continue;
}
auto const labelend{line.find("node")};
auto const nodedata{(labelend == std::string::npos ? "" : labelend == 0 ? line : line.substr(labelend))};
auto const label{(labelend == std::string::npos ? line : labelend == 0 ? generate_node_label(nodedata) : line.substr(0, labelend))};
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};
while (groupbegin != m_nodebank.end())
{
groupbegin = std::find_if(groupend, m_nodebank.end(), [](auto const &Entry) { return false == Entry.second->empty(); });
groupend = std::find_if(groupbegin, m_nodebank.end(), [](auto const &Entry) { return Entry.second->empty(); });
std::sort(groupbegin, groupend, [](auto const &Left, auto const &Right) { return Left.first < Right.first; });
}
}
void nodebank_panel::nodebank_reload()
{
m_nodebank.clear();
// images made since are read anew
if (m_previews)
{
m_previews->clear();
}
if (m_largepreviews)
{
m_largepreviews->clear();
}
std::ifstream file;
file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary);
std::string line;
while (std::getline(file, line))
{
if (line.size() < 4)
{
continue;
}
auto const labelend{line.find("node")};
auto const nodedata{(labelend == std::string::npos ? "" : labelend == 0 ? line : line.substr(labelend))};
auto const label{(labelend == std::string::npos ? line : labelend == 0 ? generate_node_label(nodedata) : line.substr(0, labelend))};
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};
while (groupbegin != m_nodebank.end())
{
groupbegin = std::find_if(groupend, m_nodebank.end(), [](auto const &Entry) { return false == Entry.second->empty(); });
groupend = std::find_if(groupbegin, m_nodebank.end(), [](auto const &Entry) { return Entry.second->empty(); });
std::sort(groupbegin, groupend, [](auto const &Left, auto const &Right) { return Left.first < Right.first; });
}
}
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
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";
m_templatepreviews.try_emplace(*entry.second, name + ".png");
}
}
void nodebank_panel::preview_popup(std::pair<std::string, std::shared_ptr<std::string>> const &Entry)
{
// 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;
}
// the generator makes the images 1024 pixels big; they're shown half as big, as much as the window has room for
auto const size{std::clamp(static_cast<int>(512.0f * Global.ui_scale), 256, 1024)};
if (m_largepreviews == nullptr)
{
m_largepreviews = std::make_unique<editor_previews::image_cache>(size, 16);
}
auto const *label{Entry.first.c_str()};
while (*label == ' ')
{
++label;
}
ImGui::BeginTooltip();
ImGui::TextUnformatted(label);
std::string model, skin;
if (editor_previews::entry_model(*Entry.second, model, skin))
{
ImGui::TextDisabled("%s%s%s", model.c_str(), (skin.empty() || skin == "none") ? "" : " ", (skin.empty() || skin == "none") ? "" : skin.c_str());
}
auto const lookup{m_previewpaths.find(Entry.second.get())};
if (lookup != m_previewpaths.end())
{
auto const side{std::min(static_cast<float>(size), ImGui::GetMainViewport()->WorkSize.y * 0.6f)};
auto texture{m_largepreviews->image(lookup->second)};
if (texture == 0 && m_previews)
{
// the small image, until the big one is read
texture = m_previews->image(lookup->second);
}
if (texture != 0)
{
ImGui::Image(static_cast<ImTextureID>(texture), ImVec2(side, side));
}
else
{
ImGui::TextDisabled("%s", m_largepreviews->missing(lookup->second) ? STR_C("no preview") : "...");
}
}
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 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);
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 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;
}
drag_entry(entry);
if (ImGui::IsItemHovered())
{
preview_popup(entry);
}
ImGui::PopID();
}
}
}
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)
{
return;
}
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
if (size.x > 0)
{
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
}
else
{
ImGui::SetNextWindowSize(ImVec2S(440, 400), ImGuiCond_FirstUseEver);
}
if (size_min.x > 0)
{
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y));
}
if (m_standin)
{
// the same window, so it keeps its place in the dock
if (ImGui::Begin((m_standintitle + "###" + name()).c_str(), nullptr, flags))
m_standin();
ImGui::End();
return;
}
auto const panelname{(title.empty() ? name() : title) + "###" + name()};
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{
if (m_largepreviews)
{
m_largepreviews->update();
}
if (ImGui::Button(STR_C("Reload node bank")))
{
nodebank_reload();
}
ImGui::SameLine();
if (ImGui::Button(m_setsopen ? "Close model sets" : "Model sets..."))
{
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
ImGui::Separator();
ImGui::PushItemWidth(-1);
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 (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"));
}
}
// 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
{
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())
{
categories.push_back({&entry.first, {}});
continue;
}
if (categories.empty())
{
// 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;
}
}
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();
ImGui::TableSetColumnIndex(1);
if (ImGui::BeginChild("##entries", ImVec2(0, paneheight)))
{
if (m_category == scenery_templates)
{
render_templates_list(searchfilter);
}
else if (false == searchfilter.empty())
{
// 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)
{
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);
}
}
}
else if (false == categories.empty())
{
if (previews)
{
render_cards(categories[m_category].entries);
}
else
{
render_list(categories[m_category].entries);
}
}
}
ImGui::EndChild();
ImGui::EndTable();
}
ImGui::PopItemWidth();
}
ImGui::End();
if (m_setsopen)
{
render_sets_window();
}
}
bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const *Manuallabel)
{
auto changed{false};
auto const preview{set_name(Ref, Manuallabel)};
if (false == ImGui::BeginCombo(Label, preview.c_str()))
{
return false;
}
if (Manuallabel != nullptr)
{
if (ImGui::Selectable(Manuallabel, Ref.kind == model_set_ref::source::manual))
{
Ref = model_set_ref{};
changed = true;
}
}
ImGui::TextDisabled(STR_C("User sets"));
auto const &sets{EditorModelSets.sets()};
if (sets.empty())
{
ImGui::TextDisabled(STR_C(" (none yet, see Model sets...)"));
}
for (auto const &set : sets)
{
auto const label{" " + set.name + " (" + std::to_string(set.templates.size()) + ")##userset" + std::to_string(set.id)};
if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::user && Ref.id == set.id))
{
Ref = {model_set_ref::source::user, set.id};
changed = true;
}
}
ImGui::TextDisabled(STR_C("Node bank groups"));
auto const groups{group_names()};
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
{
auto const label{" " + groups[idx] + "##bankgroup" + std::to_string(idx)};
if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::nodebank && Ref.id == idx))
{
Ref = {model_set_ref::source::nodebank, idx};
changed = true;
}
}
ImGui::EndCombo();
return changed;
}
void nodebank_panel::manual_list(char const *Id, std::vector<std::string> &List, int &Selected)
{
ImGui::PushID(Id);
if (ImGui::BeginListBox("##list", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 6.5f)))
{
for (int idx = 0; idx < static_cast<int>(List.size()); ++idx)
{
auto const label{generate_node_label(List[idx]) + "##" + std::to_string(idx)};
if (list_item(label.c_str(), Selected == idx))
Selected = idx;
}
if (List.empty())
{
ImGui::TextDisabled(STR_C("(empty)"));
}
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())
List.push_back(*m_selectedtemplate);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list"));
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Remove")) && Selected >= 0 && Selected < static_cast<int>(List.size()))
{
List.erase(List.begin() + Selected);
Selected = -1;
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Clear")))
{
List.clear();
Selected = -1;
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Save as set")) && false == List.empty())
{
open_sets_window(EditorModelSets.create("New set", List));
}
ImGui::PopID();
}
std::vector<std::string const *> nodebank_panel::set_entries(model_set_ref const &Ref) const
{
std::vector<std::string const *> entries;
if (Ref.kind == model_set_ref::source::user)
{
if (auto const *set = EditorModelSets.find(Ref.id))
{
for (auto const &entry : set->templates)
entries.push_back(&entry);
}
}
else if (Ref.kind == model_set_ref::source::nodebank && Ref.id >= 0)
{
// same grouping as group_templates(), without copying the templates
int group{0};
bool started{false};
for (auto const &entry : m_nodebank)
{
if (entry.second->empty())
{
if (started)
++group;
started = true;
continue;
}
started = true;
if (group == Ref.id)
entries.push_back(entry.second.get());
else if (group > Ref.id)
break;
}
}
return entries;
}
std::string const *nodebank_panel::random_template(model_set_ref const &Ref) const
{
auto const entries{set_entries(Ref)};
if (entries.empty())
return nullptr;
std::uniform_int_distribution<std::size_t> dist(0, entries.size() - 1);
return entries[dist(Global.local_random_engine)];
}
std::string nodebank_panel::set_name(model_set_ref const &Ref, char const *Manuallabel) const
{
switch (Ref.kind)
{
case model_set_ref::source::user:
{
auto const *set = EditorModelSets.find(Ref.id);
return set != nullptr ? set->name : "(deleted set)";
}
case model_set_ref::source::nodebank:
{
auto const groups{group_names()};
return Ref.id >= 0 && Ref.id < static_cast<int>(groups.size()) ? "Node bank: " + groups[Ref.id] : "(missing node bank group)";
}
default:
return Manuallabel != nullptr ? Manuallabel : "(none)";
}
}
void nodebank_panel::open_sets_window(int const Setid)
{
m_setsopen = true;
if (Setid != 0)
{
m_setsselected = Setid;
m_setsentry = -1;
}
}
void nodebank_panel::render_sets_window()
{
ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin((std::string(STR_C("Model sets")) + "###modelsets").c_str(), &m_setsopen, ImGuiWindowFlags_NoCollapse))
{
ImGui::End();
return;
}
auto const &sets{EditorModelSets.sets()};
if (EditorModelSets.find(m_setsselected) == nullptr)
{
m_setsselected = sets.empty() ? 0 : sets.front().id;
m_setsentry = -1;
}
// left side: the sets
auto const buttonsheight{ImGui::GetFrameHeightWithSpacing()};
ImGui::BeginGroup();
// 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)};
if (list_item(label.c_str(), set.id == m_setsselected))
{
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())
{
ImGui::TextDisabled(STR_C("No sets yet"));
}
ImGui::EndChild();
if (ImGui::Button(STR_C("New")))
{
open_sets_window(EditorModelSets.create("New set"));
}
auto const *edited{EditorModelSets.find(m_setsselected)};
if (edited != nullptr)
{
ImGui::SameLine();
if (ImGui::Button(STR_C("Duplicate")))
{
open_sets_window(EditorModelSets.create(edited->name + " copy", edited->templates));
edited = EditorModelSets.find(m_setsselected);
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Delete")))
{
ImGui::OpenPopup("##deleteset");
}
if (ImGui::BeginPopup("##deleteset"))
{
ImGui::Text(STR_C("Delete set \"%s\"?"), edited->name.c_str());
if (ImGui::Button(STR_C("Delete##confirm")))
{
EditorModelSets.remove(edited->id);
edited = nullptr;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
}
ImGui::EndGroup();
ImGui::SameLine();
// right side: content of the selected set
ImGui::BeginGroup();
if (edited == nullptr)
{
ImGui::TextDisabled(STR_C("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list."));
ImGui::TextDisabled(STR_C("Sets can be chosen in Insert (random model), Brush and Area fill."));
}
else
{
if (m_setsnameid != edited->id)
{
// selection changed, load its name into the edit buffer
std::snprintf(m_setsname, sizeof(m_setsname), "%s", edited->name.c_str());
m_setsnameid = edited->id;
}
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Rename").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)};
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button(STR_C("Rename")) || entered)
{
EditorModelSets.rename(edited->id, m_setsname);
m_setsnameid = 0; // reload, the name could have been adjusted to stay unique
}
ImGui::BeginChild("##setentries", ImVec2(0, -buttonsheight * 2), true);
// with the previews of the models, as in the node bank
if (EditorSettings.nodebank_previews() && m_previews != nullptr)
{
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")))
{
if (node_selected())
EditorModelSets.add(setid, {*m_selectedtemplate});
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list"));
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Remove")) && m_setsentry >= 0)
{
EditorModelSets.erase(setid, static_cast<std::size_t>(m_setsentry));
m_setsentry = -1;
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Clear")))
{
ImGui::OpenPopup("##clearset");
}
if (ImGui::BeginPopup("##clearset"))
{
ImGui::TextUnformatted(STR_C("Remove all templates from this set?"));
if (ImGui::Button(STR_C("Clear##confirm")))
{
EditorModelSets.clear(setid);
m_setsentry = -1;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
// whole node bank group at once
ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Add group").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x);
auto const groups{group_names()};
m_setsgroup.id = groups.empty() ? -1 : std::clamp(m_setsgroup.id, 0, static_cast<int>(groups.size()) - 1);
if (ImGui::BeginCombo("##addgroup", m_setsgroup.id >= 0 ? groups[m_setsgroup.id].c_str() : "(no node bank groups)"))
{
for (int idx = 0; idx < static_cast<int>(groups.size()); ++idx)
{
auto const label{groups[idx] + "##addgroup" + std::to_string(idx)};
if (ImGui::Selectable(label.c_str(), idx == m_setsgroup.id))
m_setsgroup.id = idx;
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button(STR_C("Add group")))
{
std::vector<std::string> templates;
for (auto const *entry : set_entries(m_setsgroup))
templates.push_back(*entry);
EditorModelSets.add(setid, templates);
}
}
ImGui::EndGroup();
ImGui::End();
}
void nodebank_panel::add_template(const std::string &desc)
{
auto const label{generate_node_label(desc)};
m_nodebank.push_back({label, std::make_shared<std::string>(desc)});
std::ofstream file;
file.open("nodebank.txt", std::ios_base::out | std::ios_base::app | std::ios_base::binary);
file << label << " " << desc;
}
const std::string *nodebank_panel::get_active_template()
{
return m_selectedtemplate.get();
}
bool nodebank_panel::node_selected() const
{
// scenery templates are placed with include directives, the tools which scatter nodes have no use for them
return m_selectedtemplate && false == m_selectedtemplate->empty() && false == m_selectedtemplate->starts_with(editor_includes::directive_mark);
}
std::vector<std::string> nodebank_panel::group_names() const
{
std::vector<std::string> names;
for (auto const &entry : m_nodebank)
{
if (entry.second->empty())
names.emplace_back(entry.first);
else if (names.empty())
names.emplace_back("(ungrouped)"); // templates listed before the first header
}
return names;
}
std::vector<std::string> nodebank_panel::group_templates(std::size_t const Group) const
{
std::vector<std::string> templates;
std::size_t group{0};
bool started{false};
for (auto const &entry : m_nodebank)
{
if (entry.second->empty())
{
// header: opens the next group, unless it's the very first entry
if (started)
++group;
started = true;
continue;
}
started = true;
if (group == Group)
templates.emplace_back(*entry.second);
else if (group > Group)
break;
}
return templates;
}
std::string nodebank_panel::generate_node_label(std::string Input) const
{
auto tokenizer{cParser(Input)};
tokenizer.getTokens(9, false); // skip leading tokens
auto model{tokenizer.getToken<std::string>(false)};
auto texture{tokenizer.getToken<std::string>(false)};
replace_slashes(model);
erase_extension(model);
replace_slashes(texture);
return texture == "none" ? model : model + " (" + texture + ")";
}
void functions_panel::update(scene::basic_node const *Node)
{
m_node = Node;
if (false == is_open)
{
return;
}
text_lines.clear();
m_grouplines.clear();
std::string textline;
// scenario inspector
auto const *node{Node};
auto const &camera{Global.pCamera};
}
void functions_panel::render()
{
if (false == is_open)
{
return;
}
auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0);
if (size.x > 0)
{
ImGui::SetNextWindowSize(ImVec2S(size.x, size.y));
}
if (size_min.x > 0)
{
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2(size_max.x, size_max.y));
}
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))
{
// header section
render_controls();
for (auto const &line : text_lines)
{
ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str());
}
}
ImGui::End();
}
void functions_panel::render_controls()
{
// order matches rotation_mode
char const *const modes[] = {STR_C("Random"), STR_C("Fixed"), STR_C("Default (from template)")};
auto current{static_cast<int>(rot_mode)};
if (ImGui::Combo(STR_C("Rotation"), &current, modes, IM_ARRAYSIZE(modes)))
{
rot_mode = static_cast<rotation_mode>(current);
}
if (rot_mode == FIXED)
{
// ImGui::Checkbox(STR_C("Get rotation from last object"), &rot_from_last);
ImGui::SliderFloat(STR_C("Rotation value"), &rot_value, 0.0f, 360.0f, STR_C("%.1f deg"));
}
}
namespace
{
std::pair<char const *, scene::layer_item> const layer_itemlabels[] = {{"models", scene::layer_item::model},
{"tracks", scene::layer_item::track},
{"traction", scene::layer_item::traction},
{"power sources", scene::layer_item::powersource},
{"memory cells", scene::layer_item::memcell},
{"event launchers", scene::layer_item::launcher},
{"events", scene::layer_item::event},
{"vehicles", scene::layer_item::vehicle},
{"sounds", scene::layer_item::sound},
{"terrain shapes", scene::layer_item::shape},
{"lines", scene::layer_item::lines}};
// what the layer holds, a line per kind of item
std::string layer_content(scene::basic_layer const &Layer)
{
std::string content;
for (auto const &itemlabel : layer_itemlabels)
{
auto const count{Layer.items[static_cast<std::size_t>(itemlabel.second)]};
if (count > 0)
{
content += (content.empty() ? "" : "\n") + std::string{itemlabel.first} + ": " + std::to_string(count);
}
}
return content;
}
std::string layer_name(scene::layer_handle const Layer)
{
return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : "(none)";
}
// button which can be greyed out, with an explanation shown when it's hovered
bool action_button(char const *Label, bool const Enabled, std::string const &Reason = std::string())
{
if (false == Enabled)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
auto const clicked{ImGui::Button(Label) && Enabled};
if (false == Enabled)
{
ImGui::PopStyleVar();
if (false == Reason.empty() && ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", Reason.c_str());
}
}
return clicked;
}
// text field bound to a string
bool input_text(char const *Label, std::string &Value)
{
std::array<char, 256> buffer{};
Value.copy(buffer.data(), buffer.size() - 1);
if (false == ImGui::InputText(Label, buffer.data(), buffer.size()))
{
return false;
}
Value = buffer.data();
return true;
}
ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f};
} // namespace
void itemproperties_panel::render_include()
{
auto &include{*m_include};
include.active = false;
ImGui::Text(STR_C("include: %s"), Bezogonkow(include.target).c_str());
if (false == include.info.name.empty())
{
ImGui::TextDisabled("%s", include.info.name.c_str());
}
if (false == include.issue.empty())
{
ImGui::PushTextWrapPos(0.0f);
ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(include.issue).c_str());
ImGui::PopTextWrapPos();
return;
}
if (include.values.empty())
{
ImGui::TextDisabled(STR_C("The template takes no parameters."));
return;
}
ImGui::PushItemWidth(ImGui::GetFontSize() * 14.0f);
for (std::size_t idx = 0; idx < include.values.size(); ++idx)
{
auto const id{static_cast<int>(idx + 1)};
auto const lookup{std::find_if(std::begin(include.info.parameters), std::end(include.info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })};
auto const described{lookup != std::end(include.info.parameters)};
auto const role{described ? lookup->role : std::string{"free"}};
// the label from the description, or what the description tells about the parameter
auto const label{described && false == lookup->label.empty() ? lookup->label : role != "free" ? role : "(p" + std::to_string(id) + ")"};
auto &value{include.values[idx]};
ImGui::PushID(id);
char *end{nullptr};
auto number{std::strtod(value.c_str(), &end)};
if ((role.starts_with("pos.") || role.starts_with("rot.") || role == "number") && false == value.empty() && *end == '\0')
{
if (ImGui::DragScalar(label.c_str(), ImGuiDataType_Double, &number, role.starts_with("pos.") ? 0.05f : 0.5f, nullptr, nullptr, "%.3f"))
{
value = editor_includes::number(number);
include.changed = true;
}
}
else
{
// applied once the field is left, so the template isn't processed with a half-typed value
input_text(label.c_str(), value);
if (ImGui::IsItemDeactivatedAfterEdit())
{
include.changed = true;
}
}
include.active |= ImGui::IsItemActive();
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("(p%d), %s", id, role.c_str());
}
ImGui::PopID();
}
ImGui::PopItemWidth();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped(STR_C("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again."));
ImGui::PopStyleColor();
}
void layers_panel::report(std::string const &Status, bool const Error)
{
status = Status;
status_error = Error;
}
void layers_panel::render()
{
if (false == is_open)
{
return;
}
// initially next to the toolset window
ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver);
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
{
if (scene::Layers.empty())
{
// layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping
ImGui::TextDisabled(STR_C("Scenery layers are available in an edit session.\nStart the simulator with: -edit <scenery file>"));
}
else
{
if (false == scene::Layers.listed(m_selected))
{
m_selected = null_handle;
}
auto const selected{m_selected != null_handle};
auto const removed{selected && scene::Layers.layer(m_selected).removed};
std::string reason{STR("Select a layer first")};
if (ImGui::Button(STR_C("New...")))
{
m_newname[0] = '\0';
m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1});
m_popuperror.clear();
ImGui::OpenPopup(STR_C("New layer"));
}
ImGui::SameLine();
if (removed)
{
if (ImGui::Button(STR_C("Restore")))
{
scene::Layers.restore(m_selected);
report(format(STR_C("Layer \"%s\" restored"), layer_name(m_selected).c_str()));
}
}
else if (action_button(STR_C("Remove..."), selected && scene::Layers.can_remove(m_selected, reason), reason))
{
ImGui::OpenPopup(STR_C("Remove layer"));
}
ImGui::SameLine();
// the scenario file is what holds the scenery together, it stays where it is
auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())};
if (action_button(STR_C("Merge into..."), mergeable, selected && false == removed ? STR("The scenario file can't be merged into another layer") : STR("Select a layer first")))
{
// the layer which includes the selected one is the most likely target
m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent);
if (false == scene::Layers.can_merge(m_selected, m_mergetarget, reason))
{
m_mergetarget = null_handle;
}
ImGui::OpenPopup(STR_C("Merge layer"));
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Save")) && save)
{
save();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Writes changed nodes and layers to the scenery files (Ctrl+S)"));
}
ImGui::Separator();
render_list();
if (false == status.empty())
{
ImGui::PushTextWrapPos(0.0f);
if (status_error)
{
ImGui::TextColored(status_errorcolor, "%s", status.c_str());
}
else
{
ImGui::TextUnformatted(status.c_str());
}
ImGui::PopTextWrapPos();
}
render_popups();
}
}
ImGui::End();
}
void layers_panel::render_list()
{
// the list takes the window except for the room left for the status
ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f));
ImGui::TextDisabled(STR_C("visible, locked, active, layer file"));
for (auto const handle : scene::Layers.tree())
{
// NOTE: copies of the flags, as the calls below can change the layer
auto const &layer{scene::Layers.layer(handle)};
auto const isactive{handle == scene::Layers.active()};
auto const isremoved{layer.removed};
std::string readonly;
auto const iswritable{scene::Layers.writable(handle, &readonly)};
ImGui::PushID(static_cast<int>(handle));
if (isremoved)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
// active layer receives new nodes, so it can't be hidden nor locked. removed layer is out of the picture altogether
auto const fixed{isactive || isremoved};
auto visible{layer.visible};
if (ImGui::Checkbox("##visible", &visible) && false == fixed)
{
scene::Layers.visible(handle, visible);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer");
}
ImGui::SameLine();
auto locked{layer.locked};
if (ImGui::Checkbox("##locked", &locked) && false == fixed)
{
scene::Layers.locked(handle, locked);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport");
}
ImGui::SameLine();
if (ImGui::RadioButton("##active", isactive) && false == isactive)
{
if (std::string reason; false == scene::Layers.accepts(handle, &reason))
{
report(format(STR_C("Layer \"%s\" can't take new nodes"), layer_name(handle).c_str()) + (reason.empty() ? "" : ": " + reason), true);
}
else
{
scene::Layers.active(handle);
}
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Active layer: nodes created in the editor are placed in it"));
}
ImGui::SameLine();
auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
if (layer.created)
{
label += " [new]";
}
if (isremoved)
{
label += " [removed]";
}
else if (false == iswritable)
{
label += " [read-only]";
}
else if (layer.late)
{
label += " [after FirstInit]";
}
if (ImGui::Selectable(label.c_str(), handle == m_selected))
{
m_selected = handle;
}
if (ImGui::IsItemHovered())
{
std::string content{layer.created ? STR("New layer, its file is created on save") : isremoved ? STR("Dropped from the scenery on save") : STR("Scenery file")};
if (scene::Layers.valid(layer.parent))
{
content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent));
}
if (false == iswritable)
{
content += "\nread-only: " + readonly;
}
if (layer.late)
{
content += "\nloaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there";
}
if (auto const items{layer_content(layer)}; false == items.empty())
{
content += "\n" + items;
}
ImGui::SetTooltip("%s", content.c_str());
}
if (isremoved)
{
ImGui::PopStyleVar();
}
ImGui::PopID();
}
ImGui::EndChild();
}
void layers_panel::render_popups()
{
if (ImGui::BeginPopupModal(STR_C("New layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted(STR_C("File of the new layer, in the scenery directory:"));
ImGui::InputTextWithHint("##name", STR_C("name.scm"), m_newname, IM_ARRAYSIZE(m_newname));
if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str()))
{
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
if (false == scene::Layers.listed(handle) || scene::Layers.layer(handle).removed || scene::Layers.layer(handle).binary)
{
continue;
}
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_newparent))
{
m_newparent = handle;
}
}
ImGui::EndCombo();
}
if (false == m_popuperror.empty())
{
ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str());
}
if (ImGui::Button(STR_C("Create")))
{
auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)};
if (created != null_handle)
{
m_selected = created;
report(format(STR_C("Layer \"%s\" created and made active. Its file is written on save."), layer_name(created).c_str()));
ImGui::CloseCurrentPopup();
}
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal(STR_C("Remove layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
ImGui::CloseCurrentPopup();
}
else
{
auto const &layer{scene::Layers.layer(m_selected)};
auto included{0};
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
if (handle != m_selected && scene::Layers.listed(handle) && scene::Layers.is_descendant(handle, m_selected))
{
++included;
}
}
ImGui::Text(STR_C("Remove layer \"%s\" from the scenery?"), layer_name(m_selected).c_str());
ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk.");
if (included > 0)
{
ImGui::Text(STR_C("%d layer(s) it includes are removed with it."), included);
}
if (auto const items{layer_content(layer)}; false == items.empty())
{
ImGui::Separator();
ImGui::TextUnformatted(STR_C("The layer holds:"));
ImGui::TextUnformatted(items.c_str());
ImGui::TextDisabled(STR_C("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells."));
}
if (ImGui::Button(STR_C("Remove")))
{
auto const name{layer_name(m_selected)};
if (scene::Layers.remove(m_selected))
{
report(format(STR_C("Layer \"%s\" removed. It can be restored until the scenery is saved."), name.c_str()));
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal(STR_C("Merge layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
ImGui::CloseCurrentPopup();
}
else
{
ImGui::Text(STR_C("Move content of layer \"%s\" to:"), layer_name(m_selected).c_str());
std::string reason;
auto targets{0};
if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str()))
{
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
if (false == scene::Layers.listed(handle) || false == scene::Layers.can_merge(m_selected, handle, reason))
{
continue;
}
++targets;
if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_mergetarget))
{
m_mergetarget = handle;
}
}
if (targets == 0)
{
ImGui::TextDisabled(STR_C("no layer can take it"));
}
ImGui::EndCombo();
}
ImGui::TextDisabled(STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n"
"Merged into the layer which includes it, the content stays where the include was.\n"
"Merged into any other layer it's placed at its end, which changes the load order."));
auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)};
if (action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer")))
{
auto const name{layer_name(m_selected)};
auto const target{m_mergetarget};
if (scene::Layers.merge(m_selected, target))
{
report(format(STR_C("Layer \"%s\" merged into \"%s\". The files are changed on save."), name.c_str(), layer_name(target).c_str()));
m_selected = target;
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
{
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
void includes_panel::open(std::string const &File)
{
m_file = File;
m_statuserror = false;
m_parameters = 0;
std::string error;
m_loaded = editor_includes::load(File, m_info, error);
if (false == m_loaded)
{
m_status = error;
m_statuserror = true;
return;
}
auto const described{false == m_info.name.empty() || false == m_info.parameters.empty()};
// every parameter the template uses gets an entry, described or not
m_parameters = editor_includes::parameter_count(File);
for (auto id = 1; id <= m_parameters; ++id)
{
m_info.parameter(id);
}
m_status = "The template uses " + std::to_string(m_parameters) + " parameter(s).";
if (false == described)
{
// starting point for a new description
editor_includes::suggest(File, m_info);
m_status += " It has no description yet; the roles are a suggestion based on how the template uses the parameters.";
}
}
void includes_panel::render_list()
{
if (false == EditorIncludes.scanned())
{
EditorIncludes.scan();
}
auto const &entries{EditorIncludes.entries()};
ImGui::PushItemWidth(ImGui::GetFontSize() * 12.0f);
auto changed{ImGui::InputTextWithHint("##filter", STR_C("Search templates"), m_filter, IM_ARRAYSIZE(m_filter))};
ImGui::PopItemWidth();
ImGui::SameLine();
changed |= ImGui::Checkbox(STR_C("Described"), &m_describedonly);
if (false == scene::Layers.templates().empty())
{
// known only for scenery opened for editing
ImGui::SameLine();
changed |= ImGui::Checkbox(STR_C("Used by the scenery"), &m_usedonly);
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Rescan")))
{
EditorIncludes.scan();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Looks through the scenery directory again, for the templates added or changed outside of the editor"));
}
if (changed || m_listrevision != EditorIncludes.revision())
{
m_listrevision = EditorIncludes.revision();
m_listed.clear();
m_described = 0;
std::string const filter{m_filter};
auto const &used{scene::Layers.templates()};
for (std::size_t idx = 0; idx < entries.size(); ++idx)
{
auto const &entry{entries[idx]};
m_described += (entry.described ? 1 : 0);
if ((m_describedonly && false == entry.described) || (m_usedonly && false == used.empty() && used.count(entry.file) == 0))
{
continue;
}
if (false == filter.empty() && false == contains(entry.file, filter) && false == contains(entry.name, filter) && false == contains(entry.category, filter))
{
continue;
}
m_listed.emplace_back(idx);
}
}
ImGui::TextDisabled(STR_C("%d template(s) in the scenery directory, %d described, %d ready for the node bank"), static_cast<int>(entries.size()), m_described,
static_cast<int>(EditorIncludes.ready().size()));
ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 9.0f), true);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(m_listed.size()));
while (clipper.Step())
{
for (auto row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
{
auto const &entry{entries[m_listed[row]]};
auto const marker{entry.complete ? "[ready] " : entry.described ? "[incomplete] " : ""};
auto const label{marker + Bezogonkow(entry.file) + (entry.name.empty() ? "" : " - " + entry.name) + "##" + std::to_string(m_listed[row])};
if (false == entry.described)
{
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
}
auto const clicked{list_item(label.c_str(), entry.file == m_file)};
if (false == entry.described)
{
ImGui::PopStyleColor();
}
if (false == entry.complete && false == entry.issue.empty() && ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Not in the node bank: %s"), Bezogonkow(entry.issue).c_str());
}
if (clicked)
{
open(entry.file);
}
}
}
if (m_listed.empty())
{
ImGui::TextDisabled(entries.empty() ? "(no .inc files in the scenery directory)" : "(no match)");
}
ImGui::EndChild();
}
void includes_panel::render_description()
{
ImGui::Separator();
ImGui::Text("%s", Bezogonkow(m_file).c_str());
input_text("Name", m_info.name);
input_text("Category", m_info.category);
input_text("Description", m_info.description);
ImGui::Columns(4, "##parameters", false);
ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x);
ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f);
ImGui::TextDisabled(STR_C("param"));
ImGui::NextColumn();
ImGui::TextDisabled(STR_C("role"));
ImGui::NextColumn();
ImGui::TextDisabled(STR_C("label"));
ImGui::NextColumn();
ImGui::TextDisabled(STR_C("default"));
ImGui::NextColumn();
for (auto &parameter : m_info.parameters)
{
ImGui::PushID(parameter.id);
ImGui::AlignTextToFramePadding();
// parameters which keep the template out of the node bank stand out
if (parameter.id <= m_parameters && parameter.value.empty() && false == editor_includes::automatic(parameter.role))
{
ImGui::TextColored(status_errorcolor, "(p%d)", parameter.id);
}
else
{
ImGui::Text("(p%d)", parameter.id);
}
ImGui::NextColumn();
ImGui::PushItemWidth(-1.0f);
if (ImGui::BeginCombo("##role", parameter.role.c_str()))
{
for (auto const &role : editor_includes::roles)
{
if (ImGui::Selectable(role.c_str(), role == parameter.role))
{
parameter.role = role;
}
}
ImGui::EndCombo();
}
ImGui::PopItemWidth();
ImGui::NextColumn();
ImGui::PushItemWidth(-1.0f);
input_text("##label", parameter.label);
ImGui::PopItemWidth();
ImGui::NextColumn();
ImGui::PushItemWidth(-1.0f);
input_text("##default", parameter.value);
ImGui::PopItemWidth();
ImGui::NextColumn();
ImGui::PopID();
}
ImGui::Columns(1);
// what the node bank is going to make of the description
ImGui::PushTextWrapPos(0.0f);
if (std::string issue; editor_includes::complete(m_info, m_parameters, &issue))
{
ImGui::TextUnformatted(STR_C("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)."));
}
else
{
ImGui::TextColored(status_errorcolor, STR_C("Not for the node bank yet: %s."), issue.c_str());
}
ImGui::PopTextWrapPos();
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n"
"Insert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\n"
"The other parameters receive their default values."));
}
if (ImGui::Button(STR_C("Suggest roles")))
{
editor_includes::suggest(m_file, m_info);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are"));
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Add parameter")))
{
m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1);
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Save to file")))
{
std::string error;
m_statuserror = false == editor_includes::save(m_file, m_info, error);
m_status = m_statuserror ? error : "Description saved in \"" + m_file + "\"";
if (false == m_statuserror)
{
EditorIncludes.update(m_file);
}
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Reload")))
{
open(m_file);
}
}
void includes_panel::render()
{
if (false == is_open)
{
return;
}
// next to the layers window where the screen is wide enough for both
auto const position{ImVec2S(970, 60)};
auto const windowsize{ImVec2S(600, 600)};
ImGui::SetNextWindowPos(position.x + windowsize.x <= ImGui::GetIO().DisplaySize.x ? position : ImVec2S(560, 110), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(windowsize, ImGuiCond_FirstUseEver);
auto const panelname{(title.empty() ? m_name : title) + "###" + m_name};
if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse))
{
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
ImGui::TextWrapped(STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; "
"the description is stored in the template file, as //$e comment lines."));
if (false == Global.editor_session)
{
ImGui::TextWrapped(STR_C("The node bank offers the templates in a scenery opened for editing (-edit)."));
}
ImGui::PopStyleColor();
render_list();
if (m_loaded)
{
render_description();
}
if (false == m_status.empty())
{
ImGui::PushTextWrapPos(0.0f);
if (m_statuserror)
{
ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(m_status).c_str());
}
else
{
ImGui::TextUnformatted(Bezogonkow(m_status).c_str());
}
ImGui::PopTextWrapPos();
}
}
ImGui::End();
}