17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-30 21:20: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

@@ -337,6 +337,7 @@ editor_mode::editor_mode() {
// the area fill settings live in the node bank window, in the tab of the fill mode
ui()->set_fill_options([this]() { render_area_fill(); });
ui()->set_gizmo_options([this]() { render_gizmo_options(); });
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
// the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included
m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector<world_triangle> &Out) {
gather_ground_triangles(Min, Max, true, 50.0f, Out);
@@ -606,6 +607,7 @@ void editor_mode::push_snapshot(scene::basic_node *node, EditorSnapshot::Action
snap.node_name = node->name();
snap.position = node->location();
snap.uuid = node->uuid;
snap.layer = node->layer();
if (auto *model = dynamic_cast<TAnimModel *>(node))
{
@@ -679,11 +681,13 @@ void editor_mode::undo_last()
redoSnap.serialized = snap.serialized;
redoSnap.position = snap.position;
redoSnap.node_ptr = nullptr;
redoSnap.layer = snap.layer;
g_redo.push_back(std::move(redoSnap));
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
if (created)
{
scene::Layers.move(created, snap.layer); // back to the layer it was deleted from, rather than the active one
created->location(snap.position);
created->Angles(snap.rotation);
m_node = created;
@@ -702,6 +706,7 @@ void editor_mode::undo_last()
current.action = snap.action;
current.node_name = snap.node_name;
current.node_ptr = target;
current.layer = target->layer();
current.position = target->location();
if (auto *model = dynamic_cast<TAnimModel *>(target))
{
@@ -758,6 +763,7 @@ void editor_mode::redo_last()
hist.serialized = snap.serialized;
hist.position = snap.position;
hist.uuid = snap.uuid;
hist.layer = snap.layer;
m_history.push_back(std::move(hist));
scene::basic_node *target = simulation::Instances.find(snap.node_name);
@@ -799,6 +805,7 @@ void editor_mode::redo_last()
TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position);
if (created)
{
scene::Layers.move(created, snap.layer);
created->location(snap.position);
created->Angles(snap.rotation);
created->Scale(snap.scale);
@@ -893,7 +900,7 @@ bool editor_mode::update()
update_camera(deltarealtime);
// drop the selection if its layer was hidden or locked in the meantime
if (m_node != nullptr && false == scene::Layers.editable(m_node->layer()))
if (m_node != nullptr && false == scene::Layers.editable(m_node))
{
m_node = nullptr;
m_dragging = false;
@@ -1497,6 +1504,15 @@ void editor_mode::create_chunked_terrain()
}
void editor_mode::save_scene_with_terrain()
{
commit_terrain();
// export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active)
export_scenery();
WriteLog("Editor: saved scene + terrain", logtype::generic);
}
void editor_mode::commit_terrain()
{
// commit authored terrain so the scenery streams it on load. if not already streaming, hand the
// manual grid chunks over to the streamer (same as toggling Stream terrain on)
@@ -1529,10 +1545,37 @@ void editor_mode::save_scene_with_terrain()
}
m_streamer.flush(); // save resident edited chunks to disk
}
// export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active)
export_scenery();
WriteLog("Editor: saved scene + terrain", logtype::generic);
void editor_mode::save()
{
if (scene::Layers.empty())
{
// without the sources tracked during the load there's no telling where the changes belong
ui()->set_status("The scenery wasn't opened for editing. Start the simulator with: -edit <scenery file>", true);
return;
}
// terrain made in the editor is kept in files of its own, the scenery only needs the directive which loads it
std::vector<std::string> rootstatements;
if (m_streamer.active() || false == m_grid_chunks.empty())
{
commit_terrain();
if (false == scene::Layers.terrain_directive())
{
rootstatements.emplace_back(
"editorterrain " + m_streamer.directory() + ' ' + std::to_string(m_streamer.cells()) + ' ' + to_string(m_streamer.cellsize(), 3) + ' '
+ std::to_string(m_streamer.radius()) + " endeditorterrain");
}
}
auto const result = scene::Layers.save(rootstatements);
if (result.success && false == rootstatements.empty())
{
scene::Layers.terrain_directive(true);
}
ui()->set_status(result.message, false == result.success);
WriteLog("Editor: " + result.message, logtype::generic);
}
void editor_mode::export_scenery()
@@ -2196,6 +2239,14 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
return;
}
// save; checked ahead of the camera keys, which have S bound to moving back
if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S)
{
if (is_press(Action))
save();
return;
}
// then internal input handling
if (m_input.keyboard.key(Key, Action))
return;
@@ -2364,9 +2415,13 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
m_node = nullptr;
// nodes of locked layers can't be selected
if (node && false == scene::Layers.editable(node->layer()))
// in a scenery opened for editing some nodes are off limits: the ones in locked layers,
// and the ones whose definitions can't be rewritten on save
if (std::string reason; node && false == scene::Layers.editable(node, &reason))
{
ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be edited: " + reason, true);
node = nullptr;
}
// ignore picks that are beyond allowed placement distance
if (node) {

View File

@@ -96,6 +96,7 @@ class editor_mode : public application_mode
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes
scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it
};
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
@@ -160,8 +161,12 @@ class editor_mode : public application_mode
void remove_grid_chunk(int Cx, int Cz);
// handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk)
void handle_chunk_edit_click(bool DeleteMode);
// commits authored terrain to disk, enables streaming, and exports the scenery (Ctrl+S)
// commits authored terrain to disk, enables streaming, and exports the scenery
void save_scene_with_terrain();
// commits authored terrain to disk and enables streaming
void commit_terrain();
// writes the changes to the files of scenery opened for editing (Ctrl+S)
void save();
// exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture
void export_scenery();
// raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode

View File

@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenenode.h"
#include "scene/scenelayers.h"
#include "rendering/renderer.h"
#include "utilities/translation.h"
editor_ui::editor_ui()
{
@@ -23,6 +24,8 @@ editor_ui::editor_ui()
// bind the panels with ui object. maybe not the best place for this but, eh
add_external_panel(&m_nodebankpanel);
add_external_panel(&m_layerspanel);
add_external_panel(&m_includespanel);
m_nodebankpanel.mode_options = [this](nodebank_panel::edit_mode const Mode) { render_mode_options(Mode); };
m_nodebankpanel.header_sections = [this]() { render_header_sections(); };
@@ -100,98 +103,55 @@ void editor_ui::render_header_sections()
m_itempropertiespanel.render_body();
ImGui::Unindent();
}
if (ImGui::CollapsingHeader("Layers"))
}
void editor_ui::render_menu_contents()
{
if (ImGui::BeginMenu(STR_C("File")))
{
ImGui::Indent();
render_layers();
ImGui::Unindent();
// changes go to the scenery files only if the scenery was loaded with its sources tracked
auto const editsession{false == scene::Layers.empty()};
if (ImGui::MenuItem(STR_C("Save"), "Ctrl+S", false, editsession) && m_save)
{
m_save();
}
if (false == editsession && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
{
ImGui::SetTooltip("%s", STR_C("Start the simulator with -edit <scenery file> to save changes to the scenery files"));
}
if (ImGui::MenuItem(STR_C("Export scenery dump"), "Ctrl+Shift+F11") && m_export)
{
m_export();
}
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("%s", STR_C("Writes the whole scene to _export files next to the scenario, the scenery files are left as they are"));
}
ImGui::EndMenu();
}
ui_layer::render_menu_contents();
if (ImGui::BeginMenu(STR_C("Mode windows")))
{
ImGui::MenuItem(STR_C("Toolset"), nullptr, &m_nodebankpanel.is_open);
ImGui::MenuItem(STR_C("Layers"), nullptr, &m_layerspanel.is_open);
ImGui::MenuItem(STR_C("Include descriptions"), nullptr, &m_includespanel.is_open);
ImGui::EndMenu();
}
}
void editor_ui::render_layers()
void editor_ui::set_file_actions(std::function<void()> Save, std::function<void()> Export)
{
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>");
return;
}
m_save = Save;
m_export = std::move(Export);
m_layerspanel.save = std::move(Save);
}
ImGui::TextDisabled("visible, locked, layer file (click: make active)");
std::pair<char const *, scene::layer_item> const 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}};
for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx)
{
auto const handle{static_cast<scene::layer_handle>(idx)};
auto const &layer{scene::Layers.layer(handle)};
auto const isactive{handle == scene::Layers.active()};
ImGui::PushID(static_cast<int>(idx));
// active layer receives new nodes, so it can't be hidden nor locked
if (isactive)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f);
}
auto visible{layer.visible};
if (ImGui::Checkbox("##visible", &visible) && false == isactive)
{
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 == isactive)
{
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");
}
if (isactive)
{
ImGui::PopStyleVar();
}
ImGui::SameLine();
// layers are listed in the order the files were first opened, so indentation alone shows the include tree
auto const label{std::string(2 * scene::Layers.depth(handle), ' ') + Bezogonkow(layer.name) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"};
if (ImGui::Selectable(label.c_str(), isactive))
{
scene::Layers.active(handle);
}
if (ImGui::IsItemHovered())
{
std::string content{isactive ? "Active layer, nodes created in the editor are placed here" : "Click to make this the active layer"};
if (layer.binary)
{
content += "\nThe file wasn't parsed, its content was loaded from binary terrain file";
}
for (auto const &itemlabel : itemlabels)
{
auto const count{layer.items[static_cast<std::size_t>(itemlabel.second)]};
if (count > 0)
{
content += "\n" + std::string{itemlabel.first} + ": " + std::to_string(count);
}
}
ImGui::SetTooltip("%s", content.c_str());
}
ImGui::PopID();
}
void editor_ui::set_status(std::string const &Status, bool const Error)
{
m_layerspanel.status = Status;
m_layerspanel.status_error = Error;
}
void editor_ui::render_rotation_controls()

View File

@@ -53,17 +53,24 @@ class editor_ui : public ui_layer
{
m_gizmooptions = std::move(Renderer);
}
// actions of the file menu, performed by the editor mode
void set_file_actions(std::function<void()> Save, std::function<void()> Export);
// shows outcome of an operation in the layers window
void set_status(std::string const &Status, bool const Error = false);
private:
// methods
void render_mode_options(nodebank_panel::edit_mode const Mode);
void render_header_sections();
// list of scenery layers: visibility, selection lock and choice of the layer receiving new nodes
void render_layers();
void render_menu_contents() override;
// members
itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window
functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs
nodebank_panel m_nodebankpanel{"Toolset", true}; // main editor window: gizmo, node properties, edit modes and the node bank
layers_panel m_layerspanel{"Layers", true};
includes_panel m_includespanel{"Include descriptions", false};
std::function<void()> m_save;
std::function<void()> m_export;
brush_object_list m_brushobjects;
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
model_set_ref m_insertset;

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

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/uilayer.h"
#include "utilities/Classes.h"
#include "editor/editorIncludeInfo.hpp"
#include <functional>
/*
@@ -148,6 +149,56 @@ class nodebank_panel : public ui_panel
model_set_ref m_setsgroup{model_set_ref::source::nodebank, 0}; // node bank group to append to the edited set
};
// scenery layers of a scenery opened for editing: their visibility, selection lock, the layer receiving new nodes,
// and layer management. the changes are applied to the scenery files on save
class layers_panel : public ui_panel
{
public:
layers_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {}
void render() override;
// saves the scenery; provided by the editor mode
std::function<void()> save;
// outcome of the most recent operation, shown at the bottom of the window
std::string status;
bool status_error{false};
private:
// methods
void render_list();
void render_popups();
void report(std::string const &Status, bool const Error = false);
// members
scene::layer_handle m_selected{null_handle}; // layer the buttons act on
char m_newname[128]{}; // file name of the layer being created
scene::layer_handle m_newparent{null_handle}; // layer which is going to include it
scene::layer_handle m_mergetarget{null_handle};
std::string m_popuperror;
};
// editor of the descriptions of *.inc scenery templates, see editorIncludeInfo.hpp
class includes_panel : public ui_panel
{
public:
includes_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {}
void render() override;
private:
// methods
void open(std::string const &File);
// members
std::string m_file; // template being edited, relative to the scenery directory
char m_filename[256]{}; // name of a template typed in by hand
include_info m_info;
bool m_loaded{false};
std::string m_status;
bool m_statuserror{false};
};
class functions_panel : public ui_panel
{