From 968594c3e927bf042596319867c4d6e8b2ab0e00 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 2 Oct 2026 23:57:35 +0200 Subject: [PATCH] feat(editor): include database, placing .inc templates from the node bank --- application/editormode.cpp | 143 ++++++++- application/editormode.h | 6 + application/editoruilayer.cpp | 2 +- application/editoruilayer.h | 2 +- application/editoruipanels.cpp | 367 ++++++++++++++++------- application/editoruipanels.h | 17 +- editor/editorIncludeInfo.cpp | 283 ++++++++++++++--- editor/editorIncludeInfo.hpp | 66 ++++ scene/scenelayers.cpp | 130 +++++++- scene/scenelayers.h | 50 +++ scene/scenelayerssave.cpp | 90 +++++- scene/scenenode.cpp | 4 +- scene/scenenode.h | 11 +- simulation/simulation.cpp | 4 + simulation/simulation.h | 3 + simulation/simulationstateserializer.cpp | 77 ++++- simulation/simulationstateserializer.h | 5 + utilities/Classes.h | 1 + utilities/parser.cpp | 12 + utilities/parser.h | 1 + 20 files changed, 1106 insertions(+), 168 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 9cc938dd..86b51e97 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -14,6 +14,7 @@ http://mozilla.org/MPL/2.0/. #include "application/application.h" #include "editor/editorSettings.hpp" #include "editor/editorModelSets.hpp" +#include "editor/editorIncludeInfo.hpp" #include "utilities/Globals.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" @@ -530,7 +531,7 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button) // picked only when a placement is due, so a random pick from a set isn't drawn every tick const std::string *src = ui()->get_active_node_template(); - if (!src) + if (!src || src->starts_with(editor_includes::directive_mark)) // scenery templates are placed one at a time return; std::string name = "editor_"; @@ -672,6 +673,15 @@ void editor_mode::undo_last() EditorSnapshot snap = m_history.back(); m_history.pop_back(); + if (snap.instance != 0) + { + // include of a scenery template was placed or removed; either is taken back by flipping its removal + toggle_include(snap.instance); + g_redo.push_back(std::move(snap)); + m_instance = 0; + return; + } + if (snap.action == EditorSnapshot::Action::Delete) { // undo delete -> recreate model @@ -754,6 +764,14 @@ void editor_mode::redo_last() EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); + if (snap.instance != 0) + { + toggle_include(snap.instance); + m_history.push_back(std::move(snap)); + m_instance = 0; + return; + } + // handle delete redo (re-delete) separately if (snap.action == EditorSnapshot::Action::Delete) { @@ -906,6 +924,10 @@ bool editor_mode::update() m_dragging = false; ui()->set_node(nullptr); } + if (m_instance != 0 && false == scene::Layers.removable(m_instance)) + { + m_instance = 0; + } simulation::Region->update_sounds(); audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); @@ -1578,6 +1600,82 @@ void editor_mode::save() WriteLog("Editor: " + result.message, logtype::generic); } +void editor_mode::toggle_include(scene::instance_handle const Instance) +{ + if (false == scene::Layers.tracked(Instance)) + return; + auto const &included = scene::Layers.instance(Instance); + if (included.dead) + { + // its directive is gone from the scenery file, and the place it was at with it + ui()->set_status("Include of \"" + *included.file + "\" was removed from the saved scenery, this can't be taken back.", true); + return; + } + scene::Layers.removed(Instance, false == included.removed); + ui()->set_status("Include of \"" + *included.file + "\" " + (included.removed ? "removed." : "restored.")); +} + +void editor_mode::place_include(std::string const &File, int const RotationMode, float const FixedRotation) +{ + if (scene::Layers.empty()) + { + // the directive has to go to a scenery file, and without the sources tracked there's no telling which + ui()->set_status("Scenery templates can be placed only in a scenery opened for editing (-edit).", true); + WriteLog("Editor: scenery templates can be placed only in a scenery opened for editing (-edit)", logtype::generic); + return; + } + include_info info; + std::string issue; + auto const parameters = editor_includes::parameter_count(File); + if (false == editor_includes::load(File, info, issue) || false == editor_includes::complete(info, parameters, &issue)) + { + ui()->set_status("Template \"" + File + "\" can't be placed: " + issue, true); + return; + } + + // the directive is written at the end of the file of the active layer, and has to make sense with the placement in effect there + auto const layer = scene::Layers.resolve(scene::Layers.active()); + if (std::string reason; false == scene::Layers.valid(layer) || scene::Layers.layer(layer).removed || false == scene::Layers.writable(layer, &reason)) + { + ui()->set_status("Template \"" + File + "\" can't be placed: " + (reason.empty() ? "there's no active layer to put it in" : "the active layer can't be saved, " + reason), true); + return; + } + auto const context = scene::Layers.layer(layer).context_end; + if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f}) + { + // a template can pass its parameters to an origin directive, which isn't affected by these, or straight to + // the nodes, which are. there's no single set of parameter values which puts both at the cursor + ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer ends with a rotation or a scale in effect. Pick another layer.", true); + return; + } + glm::dvec3 const location = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); + std::optional yaw; + if (RotationMode == functions_panel::RANDOM) + yaw = static_cast(LocalRandom(0.0, 360.0)); + else if (RotationMode == functions_panel::FIXED) + yaw = FixedRotation; + + auto const directive = editor_includes::directive(File, info, parameters, context.to_local(location), yaw); + auto const instance = scene::Layers.place(layer, File, directive); + if (instance == 0) + { + ui()->set_status("Template \"" + File + "\" can't be placed in the active layer.", true); + return; + } + // show the models of the template right away. the rest of it comes into play when the saved scenery is loaded + auto const preview = simulation::State.preview_include(directive, context, layer, instance); + + EditorSnapshot snap; + snap.instance = instance; + snap.node_name = File; + m_history.push_back(std::move(snap)); + g_redo.clear(); + + ui()->set_status( + "\"" + info.name + "\" placed in layer \"" + scene::Layers.layer(layer).name + "\": " + std::to_string(preview.first) + " model(s) shown" + + (preview.second > 0 ? ", " + std::to_string(preview.second) + " other statement(s) of the template take effect once the scenery is saved and loaded again." : ".")); +} + void editor_mode::export_scenery() { // hiding a layer clears the visibility flag of its nodes, and the flag is a part of exported node data. @@ -2196,6 +2294,7 @@ void editor_mode::exit() // drop selection so a stale/dangling node pointer isn't used on the next editor session m_node = nullptr; + m_instance = 0; m_gizmo_using = false; ui()->set_node(nullptr); @@ -2291,6 +2390,19 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_DELETE: + if (is_press(Action) && m_instance != 0) + { + // include of a scenery template. what it shows goes out of sight, the directive is erased on save + scene::Layers.removed(m_instance, true); + EditorSnapshot snap; + snap.instance = m_instance; + snap.node_name = *scene::Layers.instance(m_instance).file; + m_history.push_back(std::move(snap)); + g_redo.clear(); + ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved."); + m_instance = 0; + break; + } if (is_press(Action)) { TAnimModel *model = dynamic_cast(m_node); @@ -2414,13 +2526,25 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; m_node = nullptr; + m_instance = 0; - // 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)) + // in a scenery opened for editing selection has its limits. nodes of locked layers are off limits. + // nodes defined by a scenery template can't be rewritten one by one on save, so a click on one + // selects the include of the template as a whole + if (node && mode == nodebank_panel::MODIFY) { - ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be edited: " + reason, true); - node = nullptr; + std::string reason; + if (node->from_template() && scene::Layers.removable(node->m_instance, &reason)) + { + m_instance = node->m_instance; + ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" selected. Del removes it from the scenery."); + node = nullptr; + } + else if (node->from_template() || 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 @@ -2451,6 +2575,13 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (!src) return; + if (src->starts_with(editor_includes::directive_mark)) + { + // a scenery template rather than a single node + place_include(src->substr(editor_includes::directive_mark.size()), rotation_mode, fixed_rotation_value); + return; + } + std::string name = "editor_"; glm::dvec3 mouseOffset = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + mouseOffset); diff --git a/application/editormode.h b/application/editormode.h index b39e3408..f5248c71 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -97,6 +97,7 @@ class editor_mode : public application_mode 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 + scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -127,6 +128,7 @@ class editor_mode : public application_mode double fTime50Hz{0.0}; // bufor czasu dla komunikacji z PoKeys scene::basic_editor m_editor; scene::basic_node *m_node{nullptr}; // currently selected scene node + scene::instance_handle m_instance{0}; // currently selected include of a scenery template; these are handled as a whole bool m_takesnapshot{true}; // helper, hints whether snapshot of selected node(s) should be taken before modification bool m_dragging = false; glm::dvec3 oldPos; // world position of the last brush placement @@ -167,6 +169,10 @@ class editor_mode : public application_mode void commit_terrain(); // writes the changes to the files of scenery opened for editing (Ctrl+S) void save(); + // places include of specified scenery template at the cursor, in the active layer + void place_include(std::string const &File, int RotationMode, float FixedRotation); + // undo and redo of placing or removing an include: brings removed include back, or removes it + void toggle_include(scene::instance_handle Instance); // 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 diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 3ca3e1f1..70b6e0e1 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -136,7 +136,7 @@ void editor_ui::render_menu_contents() { 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::MenuItem(STR_C("Include database"), nullptr, &m_includespanel.is_open); ImGui::EndMenu(); } } diff --git a/application/editoruilayer.h b/application/editoruilayer.h index fb5444af..3f24d7c4 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -68,7 +68,7 @@ class editor_ui : public ui_layer 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}; + includes_panel m_includespanel{"Include database", false}; std::function m_save; std::function m_export; brush_object_list m_brushobjects; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 2f22f8f0..b93b5b0f 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -589,6 +589,38 @@ void nodebank_panel::render() ++idx; } } + // scenery templates. placing one adds an include directive to a scenery file, so they take scenery opened for editing + if (Global.editor_session && ImGui::CollapsingHeader("Scenery templates (.inc)")) + { + if (false == EditorIncludes.scanned()) + { + EditorIncludes.scan(); + } + auto const &templates{EditorIncludes.entries()}; + std::string const *category{nullptr}; + auto listed{0}; + for (auto const entryidx : EditorIncludes.ready()) + { + auto const &entry{templates[entryidx]}; + if (false == searchfilter.empty() && false == contains(entry.name, searchfilter) && false == contains(entry.file, searchfilter) && false == contains(entry.category, searchfilter)) + { + continue; + } + if (false == entry.category.empty() && (category == nullptr || *category != entry.category)) + { + ImGui::TextDisabled(" %s", entry.category.c_str()); + } + category = &entry.category; + auto const label{(entry.category.empty() ? " " : " ") + entry.name + " (" + Bezogonkow(entry.file) + ")##inc" + std::to_string(entryidx)}; + if (list_item(label.c_str(), entry.statement == m_selectedtemplate)) + m_selectedtemplate = entry.statement; + ++listed; + } + if (listed == 0) + { + ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)"); + } + } ImGui::ListBoxFooter(); } ImGui::PopItemWidth(); @@ -667,7 +699,7 @@ void nodebank_panel::manual_list(char const *Id, std::vector &List, } if (ImGui::Button("Add selected")) { - if (m_selectedtemplate && false == m_selectedtemplate->empty()) + if (node_selected()) List.push_back(*m_selectedtemplate); } if (ImGui::IsItemHovered()) @@ -882,7 +914,7 @@ void nodebank_panel::render_sets_window() auto const setid{edited->id}; if (ImGui::Button("Add selected")) { - if (m_selectedtemplate && false == m_selectedtemplate->empty()) + if (node_selected()) EditorModelSets.add(setid, {*m_selectedtemplate}); } if (ImGui::IsItemHovered()) @@ -962,6 +994,12 @@ 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 nodebank_panel::group_names() const { std::vector names; @@ -1504,6 +1542,7 @@ 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) @@ -1512,13 +1551,217 @@ void includes_panel::open(std::string const &File) 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 - auto const count{editor_includes::parameter_count(File)}; - for (auto id = 1; id <= count; ++id) + 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(count) + " parameter(s)"; + 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", "Search templates", m_filter, IM_ARRAYSIZE(m_filter))}; + ImGui::PopItemWidth(); + ImGui::SameLine(); + changed |= ImGui::Checkbox("Described", &m_describedonly); + if (false == scene::Layers.templates().empty()) + { + // known only for scenery opened for editing + ImGui::SameLine(); + changed |= ImGui::Checkbox("Used by the scenery", &m_usedonly); + } + ImGui::SameLine(); + if (ImGui::Button("Rescan")) + { + EditorIncludes.scan(); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("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("%d template(s) in the scenery directory, %d described, %d ready for the node bank", static_cast(entries.size()), m_described, + static_cast(EditorIncludes.ready().size())); + + ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 9.0f), true); + ImGuiListClipper clipper(static_cast(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("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("param"); + ImGui::NextColumn(); + ImGui::TextDisabled("role"); + ImGui::NextColumn(); + ImGui::TextDisabled("label"); + ImGui::NextColumn(); + ImGui::TextDisabled("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("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)."); + } + else + { + ImGui::TextColored(status_errorcolor, "Not for the node bank yet: %s.", issue.c_str()); + } + ImGui::PopTextWrapPos(); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("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("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 + "\""; + if (false == m_statuserror) + { + EditorIncludes.update(m_file); + } + } + ImGui::SameLine(); + if (ImGui::Button("Reload")) + { + open(m_file); + } } void includes_panel::render() @@ -1528,115 +1771,27 @@ void includes_panel::render() return; } - ImGui::SetNextWindowPos(ImVec2S(560, 110), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2S(520, 440), ImGuiCond_FirstUseEver); + // 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("Describes .inc templates and their parameters. The description is stored in the template file, as //$e comment lines."); + ImGui::TextWrapped("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("The node bank offers the templates in a scenery opened for editing (-edit)."); + } 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); - } + render_list(); 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 ¶meter : 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); - } + render_description(); } if (false == m_status.empty()) { diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 53cfe938..099dc85f 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -136,6 +136,8 @@ class nodebank_panel : public ui_panel // methods: std::string generate_node_label(std::string Input) const; void render_sets_window(); + // true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take + bool node_selected() const; // members: std::vector>> m_nodebank; char m_nodesearch[128]; @@ -178,7 +180,8 @@ class layers_panel : public ui_panel std::string m_popuperror; }; -// editor of the descriptions of *.inc scenery templates, see editorIncludeInfo.hpp +// database of the *.inc scenery templates: the templates found in the scenery directory, with the editor of their +// descriptions (see editorIncludeInfo.hpp). templates whose descriptions are complete are offered by the node bank class includes_panel : public ui_panel { @@ -190,10 +193,20 @@ class includes_panel : public ui_panel private: // methods void open(std::string const &File); + void render_list(); + void render_description(); // members + // template list + char m_filter[128]{}; + bool m_describedonly{false}; + bool m_usedonly{false}; + std::vector m_listed; // entries of the template bank matching the filter + int m_listrevision{-1}; // revision of the template bank the list was made for + int m_described{0}; + // description editor 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; + int m_parameters{0}; // number of parameters the template uses bool m_loaded{false}; std::string m_status; bool m_statuserror{false}; diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 720bab29..48c14062 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -16,7 +16,9 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include +#include #include namespace @@ -224,32 +226,11 @@ std::vector tokenize(std::string const &Text) return tokens; } -} // namespace - -include_parameter &include_info::parameter(int const Id) -{ - auto lookup{std::find_if(std::begin(parameters), std::end(parameters), [=](include_parameter const &Parameter) { return Parameter.id >= Id; })}; - if (lookup == std::end(parameters) || lookup->id != Id) - { - lookup = parameters.emplace(lookup); - lookup->id = Id; - } - return *lookup; -} - -namespace editor_includes -{ - -std::vector const roles{"free", "name", "pos.x", "pos.y", "pos.z", "rot.x", "rot.y", "rot.z", "track", "memcell", "event", "model", "texture", "number", "text"}; - -bool load(std::string const &File, include_info &Info, std::string &Error) +// reads description held by specified template text +bool parse(template_text const &Text, include_info &Info, std::string &Error) { Info = include_info(); - template_text text; - if (false == read_template(File, text, Error)) - { - return false; - } + auto const &text{Text}; try { auto const document{YAML::Load(text.yaml)}; @@ -289,6 +270,48 @@ bool load(std::string const &File, include_info &Info, std::string &Error) return true; } +// highest number of (pN) placeholder used by specified template text +int count_parameters(template_text const &Text) +{ + // the description lines may mention parameters the template doesn't use + auto content{Text.content}; + for (auto line{Text.lines.rbegin()}; line != Text.lines.rend(); ++line) + { + content.erase(line->first, line->second - line->first); + } + auto count{0}; + std::size_t length{0}; + for (auto location{content.find("(p")}; location != std::string::npos; location = content.find("(p", location + 1)) + { + count = std::max(count, placeholder(content, location, length)); + } + return count; +} + +} // namespace + +include_parameter &include_info::parameter(int const Id) +{ + auto lookup{std::find_if(std::begin(parameters), std::end(parameters), [=](include_parameter const &Parameter) { return Parameter.id >= Id; })}; + if (lookup == std::end(parameters) || lookup->id != Id) + { + lookup = parameters.emplace(lookup); + lookup->id = Id; + } + return *lookup; +} + +namespace editor_includes +{ + +std::vector const roles{"free", "name", "pos.x", "pos.y", "pos.z", "rot.x", "rot.y", "rot.z", "track", "memcell", "event", "model", "texture", "number", "text"}; + +bool load(std::string const &File, include_info &Info, std::string &Error) +{ + template_text text; + return read_template(File, text, Error) && parse(text, Info, Error); +} + bool save(std::string const &File, include_info const &Info, std::string &Error) { template_text text; @@ -428,23 +451,7 @@ int parameter_count(std::string const &File) { template_text text; std::string error; - if (false == read_template(File, text, error)) - { - return 0; - } - // the description lines may mention parameters the template doesn't use - auto content{text.content}; - for (auto line{text.lines.rbegin()}; line != text.lines.rend(); ++line) - { - content.erase(line->first, line->second - line->first); - } - auto count{0}; - std::size_t length{0}; - for (auto location{content.find("(p")}; location != std::string::npos; location = content.find("(p", location + 1)) - { - count = std::max(count, placeholder(content, location, length)); - } - return count; + return read_template(File, text, error) ? count_parameters(text) : 0; } void suggest(std::string const &File, include_info &Info) @@ -507,4 +514,198 @@ void suggest(std::string const &File, include_info &Info) } } +std::string const directive_mark{"include "}; + +bool automatic(std::string const &Role) +{ + return Role == "name" || Role.starts_with("pos.") || Role.starts_with("rot."); +} + +bool complete(include_info const &Info, int const Parameters, std::string *Issue) +{ + std::string issue; + if (Info.name.empty()) + { + issue = "the template needs a name"; + } + for (auto id = 1; id <= Parameters && issue.empty(); ++id) + { + auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; + if (lookup == std::end(Info.parameters) || (false == automatic(lookup->role) && lookup->value.empty())) + { + issue = "(p" + std::to_string(id) + ") needs a default value, or a role filled in by the editor (name, pos.*, rot.*)"; + } + } + if (false == issue.empty() && Issue != nullptr) + { + *Issue = issue; + } + return issue.empty(); +} + +std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional Yaw) +{ + auto const number = [](double const Value) { + std::ostringstream converter; + converter.imbue(std::locale::classic()); + converter << std::fixed << std::setprecision(3) << Value; + return converter.str(); + }; + std::string text{"include " + File}; + for (auto id = 1; id <= Parameters; ++id) + { + auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; + auto const role{lookup != std::end(Info.parameters) ? lookup->role : std::string{"free"}}; + auto value{lookup != std::end(Info.parameters) ? lookup->value : std::string{}}; + if (role == "pos.x") + { + value = number(Location.x); + } + else if (role == "pos.y") + { + value = number(Location.y); + } + else if (role == "pos.z") + { + value = number(Location.z); + } + else if (role == "rot.y" && Yaw) + { + value = number(*Yaw); + } + else if (role.starts_with("rot.") && value.empty()) + { + value = "0"; + } + else if (role == "name") + { + // names have to be unique in the scenery. the default value, or the name of the template, makes them recognizable + static std::mt19937 engine{std::random_device{}()}; + char suffix[16]; + std::snprintf(suffix, sizeof(suffix), "_%08x", static_cast(engine())); + value = (value.empty() ? std::filesystem::path(File).stem().string() : value) + suffix; + } + if (value.empty()) + { + // a parameter can't be left out, as the ones after it would take its place + value = "none"; + } + text += ' ' + (value.find_first_of(" \t;") != std::string::npos ? '\"' + value + '\"' : value); + } + return text + " end"; +} + } // namespace editor_includes + +editorIncludeBank EditorIncludes; + +namespace +{ + +include_entry examine(std::string const &File) +{ + include_entry entry; + entry.file = File; + entry.statement = std::make_shared(editor_includes::directive_mark + File); + template_text text; + if (false == read_template(File, text, entry.issue)) + { + return entry; + } + entry.described = false == text.lines.empty(); + if (false == entry.described) + { + entry.issue = "the template has no description"; + return entry; + } + include_info info; + if (false == parse(text, info, entry.issue)) + { + return entry; + } + entry.name = info.name; + entry.category = info.category; + entry.complete = editor_includes::complete(info, count_parameters(text), &entry.issue); + return entry; +} + +} // namespace + +void editorIncludeBank::scan() +{ + // the node bank may hold one of the templates selected, the statements stay in place across the scans + std::unordered_map> statements; + for (auto &entry : m_entries) + { + statements.emplace(entry.file, entry.statement); + } + m_entries.clear(); + m_scanned = true; + std::error_code error; + auto const root{std::filesystem::path(Global.asCurrentSceneryPath)}; + // NOTE: on an error the iterator turns into the end one + for (std::filesystem::recursive_directory_iterator file{root, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error)) + { + if (false == file->is_regular_file(error)) + { + continue; + } + // the scenery refers to its files with lower case names, relative to the scenery directory + std::string name; + try + { + name = ToLower(file->path().lexically_relative(root).generic_string()); + } + catch (std::exception const &) + { + // the name can't be expressed in the encoding the scenery files use, so nothing can include the file anyway + continue; + } + if (false == name.ends_with(".inc")) + { + continue; + } + m_entries.emplace_back(examine(name)); + if (auto const lookup{statements.find(name)}; lookup != statements.end()) + { + m_entries.back().statement = lookup->second; + } + } + std::sort(std::begin(m_entries), std::end(m_entries), [](include_entry const &Left, include_entry const &Right) { return Left.file < Right.file; }); + index(); +} + +void editorIncludeBank::index() +{ + m_ready.clear(); + for (std::size_t idx = 0; idx < m_entries.size(); ++idx) + { + if (m_entries[idx].complete) + { + m_ready.emplace_back(idx); + } + } + std::sort(std::begin(m_ready), std::end(m_ready), [this](std::size_t const Left, std::size_t const Right) { + auto const &left{m_entries[Left]}; + auto const &right{m_entries[Right]}; + return std::tie(left.category, left.name, left.file) < std::tie(right.category, right.name, right.file); + }); + ++m_revision; +} + +void editorIncludeBank::update(std::string const &File) +{ + auto const lookup{std::find_if(std::begin(m_entries), std::end(m_entries), [&](include_entry const &Entry) { return Entry.file == File; })}; + if (lookup == std::end(m_entries)) + { + m_entries.insert(std::upper_bound(std::begin(m_entries), std::end(m_entries), File, [](std::string const &Name, include_entry const &Entry) { return Name < Entry.file; }), examine(File)); + } + else + { + // the node bank may hold the template selected, keep its statement + auto const statement{lookup->statement}; + *lookup = examine(File); + lookup->statement = statement; + } + index(); +} diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 9aa47af2..1273a049 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -9,6 +9,8 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include #include #include @@ -55,5 +57,69 @@ bool save(std::string const &File, include_info const &Info, std::string &Error) int parameter_count(std::string const &File); // sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone void suggest(std::string const &File, include_info &Info); +// true if value of a parameter with specified role is supplied by the editor when the template is placed +bool automatic(std::string const &Role); +// true if the description has everything it takes to place the template straight from the node bank: a name, and +// either a default value or a role filled in by the editor for each of specified number of parameters. +// optionally tells what is missing +bool complete(include_info const &Info, int Parameters, std::string *Issue = nullptr); +// builds include directive which places specified template. Location and Yaw are the placement to pass through +// the parameters with matching roles, in the coordinates of the place the directive goes to; without Yaw the +// rotation parameters receive their default values +std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional Yaw); + +// marks node bank template which stands for a scenery template rather than for definition of a single node +extern std::string const directive_mark; } // namespace editor_includes + +// *.inc template found in the scenery directory +struct include_entry +{ + std::string file; // name of the template, relative to the scenery directory + std::string name; // from the description + std::string category; + bool described{false}; + bool complete{false}; // can be placed from the node bank, see editor_includes::complete() + std::string issue; // what keeps the template out of the node bank + std::shared_ptr statement; // node bank template standing for the include +}; + +// the scenery templates available to the editor: every *.inc file in the scenery directory. the templates whose +// descriptions are complete are offered by the node bank +class editorIncludeBank +{ + public: + // looks through the scenery directory for the templates, and reads their descriptions + void scan(); + bool scanned() const + { + return m_scanned; + } + std::vector const &entries() const + { + return m_entries; + } + // indices of the entries offered by the node bank, ordered by category and name + std::vector const &ready() const + { + return m_ready; + } + // reads description of specified template again, after it was changed + void update(std::string const &File); + // changes with each change of the entries + int revision() const + { + return m_revision; + } + + private: + void index(); + + std::vector m_entries; // sorted by file name + std::vector m_ready; + bool m_scanned{false}; + int m_revision{0}; +}; + +extern editorIncludeBank EditorIncludes; diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index abbd56d6..a3b3f51e 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -114,6 +114,8 @@ void node_layers::clear() m_sources.clear(); m_erased.clear(); m_templates.clear(); + m_instances.clear(); + m_instance = 0; m_terraindirective = false; } @@ -208,7 +210,8 @@ void node_layers::visible(layer_handle Layer, bool const Visible) } if (Visible) { - if (Node->m_layerhidden) + // nodes of a removed include stay out of sight + if (Node->m_layerhidden && (false == tracked(Node->m_instance) || false == instance(Node->m_instance).removed)) { Node->visible(true); Node->m_layerhidden = false; @@ -296,7 +299,7 @@ bool node_layers::editable(basic_node const *Node, std::string *Reason) const return true; } std::string reason; - if (Node->m_template) + if (Node->from_template()) { reason = "it's a part of an include template (.inc)"; } @@ -361,7 +364,7 @@ void node_layers::show_all() // stores origin of a node defined directly in a layer file, to allow rewriting its definition on save void node_layers::track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles, glm::vec3 const &Scale) { - if (Node == nullptr || Node->m_template || false == valid(Node->layer()) || false == Span.valid()) + if (Node == nullptr || Node->from_template() || false == valid(Node->layer()) || false == Span.valid()) { return; } @@ -606,6 +609,18 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin Reason = "the target layer is included by the merged one"; return false; } + if (false == source.context_end.matches(target.context_end)) + { + // directives of includes placed in the editor are prepared for the end of their layer file + auto const unsaved{std::any_of(std::begin(m_instances), std::end(m_instances), [&](include_instance const &Instance) { + return false == Instance.dead && false == Instance.removed && false == Instance.directive.empty() && resolve(Instance.layer) == Source; + })}; + if (unsaved) + { + Reason = "the merged layer has includes which weren't saved yet, save the scenery first"; + return false; + } + } if (source.created) { // nothing but the nodes made in the editor to move @@ -667,6 +682,115 @@ bool node_layers::pending() const return std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); }); } +// indicates start of content of a template, included by directive at specified location of the layer file being loaded +void node_layers::instance_begin(std::string const &File, source_span const &Span) +{ + if (m_instances.size() + 1 >= untracked_instance) + { + m_instance = untracked_instance; + return; + } + m_instances.emplace_back(); + auto &included{m_instances.back()}; + included.layer = handle(); + included.file = &(*m_templates.emplace(File).first); + included.span = Span; + m_instance = static_cast(m_instances.size()); +} + +// true if specified include can be removed in the editor. optionally explains why it can't +bool node_layers::removable(instance_handle Instance, std::string *Reason) const +{ + std::string reason; + if (false == tracked(Instance) || instance(Instance).dead) + { + reason = "it's a part of an include template (.inc)"; + } + else + { + auto const includelayer{resolve(instance(Instance).layer)}; + if (false == valid(includelayer) || layer(includelayer).removed) + { + reason = "its layer is no longer a part of the scenery"; + } + else if (false == writable(includelayer, &reason)) + { + // reason was filled by the call + } + else if (layer(includelayer).locked) + { + reason = "its layer is locked"; + } + } + if (false == reason.empty() && Reason != nullptr) + { + *Reason = reason; + } + return reason.empty(); +} + +// drops specified include from the scenery, or brings it back +void node_layers::removed(instance_handle Instance, bool Removed) +{ + if (false == tracked(Instance) || m_instances[Instance - 1].dead || m_instances[Instance - 1].removed == Removed) + { + return; + } + m_instances[Instance - 1].removed = Removed; + + auto const includelayer{resolve(m_instances[Instance - 1].layer)}; + auto const layervisible{false == valid(includelayer) || layer(includelayer).visible}; + auto const update_node = [=, this](basic_node *Node, layer_item const Item) { + if (Node == nullptr || Node->m_instance != Instance) + { + return; + } + count(Node->layer(), Item, Removed ? -1 : 1); + if (Removed) + { + if (Node->visible()) + { + Node->visible(false); + Node->m_layerhidden = true; + } + } + else if (Node->m_layerhidden && layervisible) + { + Node->visible(true); + Node->m_layerhidden = false; + } + }; + for (auto *modelinstance : simulation::Instances.sequence()) + { + update_node(modelinstance, layer_item::model); + } + for (auto *path : simulation::Paths.sequence()) + { + update_node(path, layer_item::track); + } + for (auto *traction : simulation::Traction.sequence()) + { + update_node(traction, layer_item::traction); + } +} + +// registers include directive made in the editor, to be written to the file of specified layer on save +instance_handle node_layers::place(layer_handle Layer, std::string const &File, std::string const &Directive) +{ + Layer = resolve(Layer); + if (false == valid(Layer) || m_instances.size() + 1 >= untracked_instance) + { + return 0; + } + m_instances.emplace_back(); + auto &included{m_instances.back()}; + included.layer = Layer; + included.file = &(*m_templates.emplace(File).first); + included.directive = Directive; + included.context = layer(Layer).context_end; + return static_cast(m_instances.size()); +} + } // namespace scene //--------------------------------------------------------------------------- diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 394f607d..44661fc5 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenenode.h" #include +#include #include namespace scene @@ -93,6 +94,21 @@ struct basic_layer std::size_t item_count() const; }; +// include directive bringing an *.inc template into a scenery layer file +struct include_instance +{ + layer_handle layer{null_handle}; // layer of the file holding the directive + std::string const *file{nullptr}; // name of the included template + source_span span; // location of the whole directive in the file; invalid for directive which wasn't saved yet + std::string directive; // text of the directive, for include placed in the editor and not saved yet + layer_context context; // placement the directive of include placed in the editor was prepared for + bool removed{false}; // the include is to be dropped from the scenery + bool dead{false}; // removal of the include was saved +}; + +// marks nodes defined by a template which was included in a way the editor doesn't keep track of +instance_handle const untracked_instance{std::numeric_limits::max()}; + // origin of a node defined in a scenery layer file, with the placement it was loaded with struct node_source { @@ -197,6 +213,18 @@ class node_layers { return m_templates; } + // indicates start of content of a template, included by directive at specified location of the layer file being loaded + void instance_begin(std::string const &File, source_span const &Span); + // indicates end of content of the template + void instance_end() + { + m_instance = 0; + } + // include whose template is being loaded, 0 if there's none + instance_handle instance() const + { + return m_instance; + } // updates placement in effect for the statements which follow void context(layer_context const &Context) { @@ -229,6 +257,26 @@ class node_layers bool merge(layer_handle Source, layer_handle Target); // true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes bool pending() const; + + // includes of *.inc templates. the editor handles each as a whole: it can be placed and removed, but not taken apart + // true if specified handle stands for an include the editor keeps track of + bool tracked(instance_handle const Instance) const + { + return Instance != 0 && Instance <= m_instances.size(); + } + // grants access to specified include. NOTE: the include has to be tracked + include_instance const &instance(instance_handle const Instance) const + { + return m_instances[Instance - 1]; + } + // true if specified include can be removed in the editor. optionally explains why it can't + bool removable(instance_handle Instance, std::string *Reason = nullptr) const; + // drops specified include from the scenery, or brings it back. its nodes are hidden right away, the directive + // is erased from the layer file on save; what else the template defines stays in the scene until the next load + void removed(instance_handle Instance, bool Removed); + // registers include directive made in the editor, to be written to the file of specified layer on save. + // the directive has to be prepared for the placement in effect at the end of that file. returns: handle to the include + instance_handle place(layer_handle Layer, std::string const &File, std::string const &Directive); // true if specified layer is included, directly or not, by the other one bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const; @@ -254,6 +302,8 @@ class node_layers std::unordered_map m_sources; // origins of nodes which can be rewritten on save std::vector> m_erased; // definitions of nodes deleted since the load or the last save std::set m_templates; // *.inc templates used by the scenery + std::vector m_instances; // includes of the templates; instance handle is index in the vector + 1 + instance_handle m_instance{0}; // helper, include whose template is being loaded bool m_terraindirective{false}; }; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index b42c7286..f56c5c11 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -294,9 +294,15 @@ struct node_layers::composition layer_handle layer; source_span span; }; + struct instance_place + { + instance_handle instance; + source_span span; + }; std::string text; std::vector nodes; std::vector sites; + std::vector instances; // adds another piece of text at the end void append(composition const &Other) @@ -315,6 +321,12 @@ struct node_layers::composition place.span.end += offset; sites.emplace_back(place); } + for (auto place : Other.instances) + { + place.span.begin += offset; + place.span.end += offset; + instances.emplace_back(place); + } } }; @@ -324,6 +336,7 @@ struct node_layers::save_state std::map patches; std::map> nodes; // unchanged nodes defined in each file std::map> includes; // unchanged include directives in each file, by included layer + std::map> instances; // unchanged includes of templates in each file std::string eol{"\r\n"}; std::string error; }; @@ -477,6 +490,13 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi Output.sites.push_back({included, shift(layer(included).sites.front().span)}); } } + if (auto const lookup{State.instances.find(Layer)}; lookup != State.instances.end()) + { + for (auto const included : lookup->second) + { + Output.instances.push_back({included, shift(instance(included).span)}); + } + } return true; } @@ -608,7 +628,7 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(Node); } - else if (Model != nullptr && false == Node->m_template) + else if (Model != nullptr && false == Node->from_template()) { // created in the editor. the definition is placed at the end of the layer file, with the placement in effect there std::string text; @@ -654,6 +674,41 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[erased.first].edits.emplace_back(std::move(edit)); } + // includes of templates + std::map> placed; // directives made in the editor, by target layer + for (std::size_t idx = 0; idx < m_instances.size(); ++idx) + { + auto const &included{m_instances[idx]}; + if (included.dead) + { + continue; + } + if (included.span.valid()) + { + if (included.removed) + { + file_patch::edit edit; + edit.begin = included.span.begin; + edit.end = included.span.end; + state.patches[included.layer].edits.emplace_back(std::move(edit)); + } + } + else if (false == included.removed && false == included.directive.empty()) + { + auto const target{resolve(included.layer)}; + if (false == is_output(target)) + { + continue; + } + if (false == included.context.matches(layer(target).context_end)) + { + // shouldn't happen, merge of layers is refused when it'd lead to this + return fail("include \"" + *included.file + "\" was placed for different origin, rotation or scale than its layer ends with"); + } + placed[target].emplace_back(static_cast(idx + 1)); + } + } + // layer changes std::map> appended; // merged layers placed at the end of their target std::set absorbed; // merged layers which cease to exist as files @@ -748,7 +803,7 @@ save_result node_layers::save(std::vector const &Rootstatements) { continue; } - if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || + if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || placed.count(candidate) != 0 || (candidate == root && false == Rootstatements.empty())) { outputs.emplace_back(candidate); @@ -782,6 +837,14 @@ save_result node_layers::save(std::vector const &Rootstatements) } state.includes[included.sites.front().parent].emplace_back(candidate); } + for (std::size_t idx = 0; idx < m_instances.size(); ++idx) + { + auto const &included{m_instances[idx]}; + if (false == included.dead && false == included.removed && included.span.valid() && state.content.count(included.layer) != 0) + { + state.instances[included.layer].emplace_back(static_cast(idx + 1)); + } + } std::map texts; for (auto const output : outputs) @@ -822,6 +885,17 @@ save_result node_layers::save(std::vector const &Rootstatements) text.text += definition.second + eol; } } + if (auto const lookup{placed.find(output)}; lookup != placed.end()) + { + for (auto const included : lookup->second) + { + auto const &directive{instance(included).directive}; + ensure_newline(text.text, eol); + auto const offset{static_cast(text.text.size())}; + text.instances.push_back({included, {offset, offset + static_cast(directive.size())}}); + text.text += directive + eol; + } + } if (output == root) { for (auto const &statement : Rootstatements) @@ -927,6 +1001,13 @@ save_result node_layers::save(std::vector const &Rootstatements) source.scale = (model != nullptr ? model->Scale() : glm::vec3{1.f}); } } + for (auto const &place : texts[output].instances) + { + auto &included{m_instances[place.instance - 1]}; + included.layer = output; + included.span = place.span; + included.directive.clear(); + } for (auto const &place : texts[output].sites) { auto &included{layer(place.layer)}; @@ -961,6 +1042,11 @@ save_result node_layers::save(std::vector const &Rootstatements) { stat(output); } + for (auto &included : m_instances) + { + // removed includes are gone from the files now + included.dead = included.dead || included.removed; + } m_erased.clear(); if (removedterrain) { diff --git a/scene/scenenode.cpp b/scene/scenenode.cpp index 788d0717..9f07a003 100644 --- a/scene/scenenode.cpp +++ b/scene/scenenode.cpp @@ -718,8 +718,8 @@ memory_node::deserialize( cParser &Input, node_data const &Nodedata ) { basic_node::basic_node( scene::node_data const &Nodedata ) : m_name( Nodedata.name ), - m_template( Nodedata.is_template ), - m_layer( Nodedata.layer ) + m_layer( Nodedata.layer ), + m_instance( Nodedata.instance ) { uuid = UID::random(); node_type = Nodedata.type; diff --git a/scene/scenenode.h b/scene/scenenode.h index 549eb0b4..8de0e060 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -71,7 +71,7 @@ struct node_data { std::string name; std::string type; layer_handle layer { null_handle }; // scenery layer the node is defined in, if any - bool is_template { false }; // the node is defined by an *.inc template rather than directly by a scenery layer file + instance_handle instance { 0 }; // include of an *.inc template which defines the node, if it isn't defined directly by a scenery layer file }; // holds unique piece of geometry, covered with single material @@ -365,6 +365,9 @@ public: layer( scene::layer_handle Layer ); scene::layer_handle layer() const; + // true if the node is defined by an *.inc template + bool + from_template() const { return m_instance != 0; } void mark_dirty() { m_dirty = true; } bool @@ -381,9 +384,11 @@ public: bool m_visible { true }; // visibility flag std::string m_name; bool m_dirty { false }; - bool m_layerhidden { false }; // the node was visible until its scenery layer was hidden - bool m_template { false }; // the node is defined by an *.inc template, which the editor can't rewrite. set only for scenery opened for editing + bool m_layerhidden { false }; // the node was visible until the editor hid its scenery layer, or the include which defines it scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any + // include of an *.inc template which defines the node; 0 if the node is defined directly by a scenery layer file. + // such nodes can't be rewritten one by one on scenery save. set only for scenery opened for editing + scene::instance_handle m_instance { 0 }; UID uuid; private: diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index ed381572..e50eff37 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -493,6 +493,10 @@ TEventLauncher * state_manager::create_eventlauncher(const std::string &src, con return m_serializer.create_eventlauncher(src, name, position); } +std::pair state_manager::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { + return m_serializer.preview_include(Directive, Context, Layer, Instance); +} + void state_manager::delete_model(TAnimModel *model) { scene::Layers.count(model->layer(), scene::layer_item::model, -1); scene::Layers.forget(model); diff --git a/simulation/simulation.h b/simulation/simulation.h index 09ea4d42..fe5fc87b 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -43,6 +43,9 @@ public: // create eventlauncher from node string TEventLauncher * create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position); + // creates model instances defined by an include directive placed in the scenery editor + std::pair + preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance); // delete TAnimModel instance void delete_model(TAnimModel *model); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index d1d44963..ae0841af 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -441,7 +441,10 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch scene::node_data nodedata; nodedata.layer = scene::Layers.handle(); - nodedata.is_template = ( nodedata.layer != null_handle ) && ( false == Input.InLayerFile() ); + if( ( nodedata.layer != null_handle ) && ( false == Input.InLayerFile() ) ) { + // defined by a template, or by something else which isn't a scenery layer file + nodedata.instance = ( scene::Layers.instance() != 0 ? scene::Layers.instance() : scene::untracked_instance ); + } // common data and node type indicator Input.getTokens( 4 ); Input @@ -1412,6 +1415,78 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st return cloned; } +std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { + // statements which take more than a single token, with the tokens ending them + static std::unordered_map const nodeends { + { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, + { "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" }, + { "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } }; + static std::unordered_map const statementends { + { "event", "endevent" }, { "trainset", "endtrainset" }, { "isolated", "endisolated" }, { "area", "endarea" }, { "assignment", "endassignment" }, + { "atmo", "endatmo" }, { "camera", "endcamera" }, { "config", "endconfig" }, { "description", "enddescription" }, { "light", "endlight" }, + { "sky", "endsky" }, { "test", "endtest" }, { "time", "endtime" }, { "terrain", "endterrain" }, { "editorterrain", "endeditorterrain" } }; + + cParser parser(Directive, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction); + // the template is processed with the placement its directive is going to be loaded with + scene::scratch_data scratch; + scratch.location.offset.emplace(Context.offset); + scratch.location.rotation = Context.rotation; + scratch.location.scale.emplace(Context.scale); + + auto created { 0 }; + auto skipped { 0 }; + auto token { parser.getToken() }; + while (false == token.empty()) { + if (token == "origin") { deserialize_origin(parser, scratch); } + else if (token == "endorigin") { deserialize_endorigin(parser, scratch); } + else if (token == "scale") { deserialize_scale(parser, scratch); } + else if (token == "endscale") { deserialize_endscale(parser, scratch); } + else if (token == "rotate") { deserialize_rotate(parser, scratch); } + else if (token == "node") { + scene::node_data nodedata; + parser.getTokens(4); + parser >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type; + if (nodedata.name == "none") { nodedata.name.clear(); } + nodedata.layer = Layer; + nodedata.instance = Instance; + auto *instance { (nodedata.type == "model" && nodedata.range_min >= 0.0) ? deserialize_model(parser, scratch, nodedata) : nullptr }; + if (instance != nullptr) { + // same as for a model loaded with the scenery + if (instance->Model() != nullptr) { + for (auto const &smokesource : instance->Model()->smoke_sources()) { + Particles.insert(smokesource.first, instance, smokesource.second); + } + } + simulation::Instances.insert(instance); + simulation::Region->insert(instance); + scene::Hierarchy[instance->uuid.to_string()] = instance; + scene::Layers.count(Layer, scene::layer_item::model); + ++created; + } + else if (nodedata.type != "model" || nodedata.range_min < 0.0) { + // NOTE: a model which failed to load is consumed up to its end already + auto const lookup { nodeends.find(nodedata.type) }; + if (lookup != nodeends.end()) { skip_until(parser, lookup->second); } + ++skipped; + } + } + else if (token == "lua") { + parser.getTokens(1, false); + ++skipped; + } + else { + auto const lookup { statementends.find(token) }; + if (lookup != statementends.end()) { + skip_until(parser, lookup->second); + ++skipped; + } + // anything else is a single token with no parameters, or something the loader wouldn't recognize either + } + token = parser.getToken(); + } + return { created, skipped }; +} + TEventLauncher *state_serializer::create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position) { cParser parser(src); parser.getTokens(); // "node" diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index a5d6f2dc..2dd64307 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/parser.h" #include "scene/scene.h" +#include "scene/scenelayers.h" namespace simulation { @@ -45,6 +46,10 @@ public: TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position); // create new eventlauncher from node stirng TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position); + // creates model instances defined by an include directive placed in the scenery editor, to show what was placed. + // everything else the included template defines is left out, and takes effect when the saved scenery is loaded. + // Context: placement in effect at the location of the directive. returns: number of created models and of skipped statements + std::pair preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance); private: // methods diff --git a/utilities/Classes.h b/utilities/Classes.h index 568e3ae9..1e91bc96 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -57,6 +57,7 @@ struct node_data; class basic_node; using group_handle = std::size_t; using layer_handle = std::uint16_t; +using instance_handle = std::uint32_t; } namespace Mtable diff --git a/utilities/parser.cpp b/utilities/parser.cpp index 36592d96..293f5156 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -124,6 +124,10 @@ cParser::~cParser() // content of the processed file ended, following nodes belong to the parent layer again scene::Layers.close(); } + if (true == mInstanceFile) + { + scene::Layers.instance_end(); + } } template <> glm::vec3 cParser::getToken(bool const ToLower, char const *Break) @@ -386,6 +390,14 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri if (mIncludeParser->mIncFile) { scene::Layers.template_used(includefile); + // include of a template made directly by a layer file is handled by the editor as a whole. templates + // included by that template are a part of it + auto const site{include_site()}; + if (false == site.fixed && scene::Layers.instance() == 0 && mIncludeParser->mSize > 0) + { + scene::Layers.instance_begin(includefile, site.span); + mIncludeParser->mInstanceFile = true; + } } else if (mIncludeParser->mSize > 0) { diff --git a/utilities/parser.h b/utilities/parser.h index c64ed131..bafcdff9 100644 --- a/utilities/parser.h +++ b/utilities/parser.h @@ -131,6 +131,7 @@ class cParser //: public std::stringstream std::size_t mLine { 0 }; // currently processed line bool mIncFile { false }; // the parser is processing an *.inc file bool mLayerFile { false }; // the parser is processing a file registered as scenery layer + bool mInstanceFile { false }; // the parser is processing a template whose include is registered with the scenery layers std::streamoff mPosition { 0 }; // amount of bytes read from the stream so far std::streamoff mTokenBegin { 0 }; // location of the most recently read token std::streamoff mTokenEnd { 0 };