17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-28 23:57:28 +01:00

feat(editor): layers window, scenery save and include descriptions

This commit is contained in:
2026-10-02 22:40:52 +02:00
parent 73f2f68aa0
commit 3199f3796e
18 changed files with 3045 additions and 123 deletions

View File

@@ -1076,4 +1076,581 @@ void functions_panel::render_controls()
// ImGui::Checkbox("Get rotation from last object", &rot_from_last);
ImGui::SliderFloat("Rotation value", &rot_value, 0.0f, 360.0f, "%.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 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("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{"Select a layer first"};
if (ImGui::Button("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("New layer");
}
ImGui::SameLine();
if (removed)
{
if (ImGui::Button("Restore"))
{
scene::Layers.restore(m_selected);
report("Layer \"" + layer_name(m_selected) + "\" restored");
}
}
else if (action_button("Remove...", selected && scene::Layers.can_remove(m_selected, reason), reason))
{
ImGui::OpenPopup("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("Merge into...", mergeable, selected && false == removed ? "The scenario file can't be merged into another layer" : "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("Merge layer");
}
ImGui::SameLine();
if (ImGui::Button("Save") && save)
{
save();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("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("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 (isremoved || false == iswritable)
{
report("Layer \"" + layer_name(handle) + "\" can't take new nodes" + (isremoved ? "" : ": " + readonly), true);
}
else
{
scene::Layers.active(handle);
}
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("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]";
}
if (ImGui::Selectable(label.c_str(), handle == m_selected))
{
m_selected = handle;
}
if (ImGui::IsItemHovered())
{
std::string content{layer.created ? "New layer, its file is created on save" : isremoved ? "Dropped from the scenery on save" : "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 (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("New layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("File of the new layer, in the scenery directory:");
ImGui::InputTextWithHint("##name", "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("Create"))
{
auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)};
if (created != null_handle)
{
m_selected = created;
report("Layer \"" + layer_name(created) + "\" created and made active. Its file is written on save.");
ImGui::CloseCurrentPopup();
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("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("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("%d layer(s) it includes are removed with it.", included);
}
if (auto const items{layer_content(layer)}; false == items.empty())
{
ImGui::Separator();
ImGui::TextUnformatted("The layer holds:");
ImGui::TextUnformatted(items.c_str());
ImGui::TextDisabled("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells.");
}
if (ImGui::Button("Remove"))
{
auto const name{layer_name(m_selected)};
if (scene::Layers.remove(m_selected))
{
report("Layer \"" + name + "\" removed. It can be restored until the scenery is saved.");
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
if (ImGui::BeginPopupModal("Merge layer", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
if (false == scene::Layers.listed(m_selected))
{
ImGui::CloseCurrentPopup();
}
else
{
ImGui::Text("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("no layer can take it");
}
ImGui::EndCombo();
}
ImGui::TextDisabled("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("Merge", possible, m_mergetarget != null_handle ? reason : "Pick the target layer"))
{
auto const name{layer_name(m_selected)};
auto const target{m_mergetarget};
if (scene::Layers.merge(m_selected, target))
{
report("Layer \"" + name + "\" merged into \"" + layer_name(target) + "\". The files are changed on save.");
m_selected = target;
}
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel"))
{
ImGui::CloseCurrentPopup();
}
}
ImGui::EndPopup();
}
}
void includes_panel::open(std::string const &File)
{
m_file = File;
m_statuserror = false;
std::string error;
m_loaded = editor_includes::load(File, m_info, error);
if (false == m_loaded)
{
m_status = error;
m_statuserror = true;
return;
}
// every parameter the template uses gets an entry, described or not
auto const count{editor_includes::parameter_count(File)};
for (auto id = 1; id <= count; ++id)
{
m_info.parameter(id);
}
m_status = "The template uses " + std::to_string(count) + " parameter(s)";
}
void includes_panel::render()
{
if (false == is_open)
{
return;
}
ImGui::SetNextWindowPos(ImVec2S(560, 110), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2S(520, 440), 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("Describes .inc templates and their parameters. The description is stored in the template file, as //$e comment lines.");
ImGui::PopStyleColor();
// templates the loaded scenery makes use of; known only for scenery opened for editing
auto const &templates{scene::Layers.templates()};
if (false == templates.empty())
{
ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 6.0f), true);
for (auto const &file : templates)
{
if (ImGui::Selectable(Bezogonkow(file).c_str(), file == m_file))
{
open(file);
}
}
ImGui::EndChild();
}
ImGui::PushItemWidth(-ImGui::CalcTextSize(" Open ").x - ImGui::GetStyle().ItemSpacing.x * 2.0f);
auto const entered{ImGui::InputTextWithHint("##file", "path of an .inc file in the scenery directory", m_filename, IM_ARRAYSIZE(m_filename), ImGuiInputTextFlags_EnterReturnsTrue)};
ImGui::PopItemWidth();
ImGui::SameLine();
if ((ImGui::Button("Open") || entered) && m_filename[0] != '\0')
{
std::string file{ToLower(m_filename)};
replace_slashes(file);
open(file);
}
if (m_loaded)
{
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("param");
ImGui::NextColumn();
ImGui::TextDisabled("role");
ImGui::NextColumn();
ImGui::TextDisabled("label");
ImGui::NextColumn();
ImGui::TextDisabled("default");
ImGui::NextColumn();
for (auto &parameter : m_info.parameters)
{
ImGui::PushID(parameter.id);
ImGui::AlignTextToFramePadding();
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);
if (ImGui::Button("Suggest roles"))
{
editor_includes::suggest(m_file, m_info);
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("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("Add parameter"))
{
m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1);
}
ImGui::SameLine();
if (ImGui::Button("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 + "\"";
}
ImGui::SameLine();
if (ImGui::Button("Reload"))
{
open(m_file);
}
}
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();
}