mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-17 12:09:19 +02:00
- 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
2420 lines
75 KiB
C++
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 ∅
|
|
|
|
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"), ¤t, 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 ¶meter : 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();
|
|
}
|