From c4a22eabd5442db60b5aaecbfa7e72836ff69754 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Tue, 6 Oct 2026 21:58:57 +0200 Subject: [PATCH] feat(editor): nodes placed with the brush and the area fill go without names; name of a model instance set in the node properties and written to its definition on save; redo of a deletion finds nodes which have no name --- application/editormode.cpp | 19 ++++---- application/editoruipanels.cpp | 82 ++++++++++++++++++++++++++++++++++ application/editoruipanels.h | 6 +++ scene/scenelayers.cpp | 12 +++++ scene/scenelayers.h | 3 ++ scene/scenelayerssave.cpp | 13 +++++- simulation/simulation.cpp | 9 ++++ simulation/simulation.h | 3 ++ utilities/Names.h | 30 ++++++++++++- 9 files changed, 166 insertions(+), 11 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index ef991c64..f5b6d716 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -757,17 +757,14 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button) if (!src || src->starts_with(editor_includes::directive_mark)) // scenery templates are placed one at a time return; - std::string name = "editor_"; - - TAnimModel *cloned = simulation::State.create_model(*src, name, placement_on_ground(newPos)); + // NOTE: no name for what the brush puts in, as with thousands of nodes the names take their share of the time + // the scenery needs to load. a node which needs one, to be referred to by an event, gets it in the node properties + TAnimModel *cloned = simulation::State.create_model(*src, std::string{}, placement_on_ground(newPos)); oldPos = newPos; m_brush_has_last = true; if (!cloned) return; - std::string new_name = "editor_" + cloned->uuid.to_string(); - - cloned->m_name = new_name; apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); std::string as_text; @@ -924,6 +921,7 @@ void editor_mode::undo_last() redoSnap.serialized = snap.serialized; redoSnap.position = snap.position; redoSnap.node_ptr = nullptr; + redoSnap.uuid = snap.uuid; redoSnap.layer = snap.layer; g_redo.push_back(std::move(redoSnap)); @@ -936,6 +934,8 @@ void editor_mode::undo_last() m_node = created; m_node->uuid = snap.uuid; // restore original UUID for better tracking (not strictly necessary) add_to_hierarchy(created); + // a node without a name can be found for the redo only this way + g_redo.back().node_ptr = created; ui()->set_node(m_node); } return; @@ -1028,7 +1028,8 @@ void editor_mode::redo_last() hist.layer = snap.layer; m_history.push_back(std::move(hist)); - scene::basic_node *target = simulation::Instances.find(snap.node_name); + // NOTE: located the way other steps do it, as the name alone doesn't lead to a node which has none + scene::basic_node *target = find_node_by_any(snap.node_ptr, snap.uuid.to_string(), snap.node_name); if (target) { if (auto *model = dynamic_cast(target)) @@ -2722,10 +2723,10 @@ void editor_mode::run_area_fill() continue; auto const &src = package[package.size() > 1 ? std::min(package.size() - 1, static_cast(LocalRandom(0.0, static_cast(package.size())))) : 0]; - TAnimModel *cloned = simulation::State.create_model(src, "editor_", location); + // NOTE: no names here either, same as with the brush + TAnimModel *cloned = simulation::State.create_model(src, std::string{}, location); if (!cloned) continue; - cloned->m_name = "editor_" + cloned->uuid.to_string(); apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); if (scalemin != 1.0f || scalemax != 1.0f) cloned->Scale(static_cast(LocalRandom(scalemin, scalemax))); diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 0ca125bd..9aa6f145 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -20,10 +20,29 @@ http://mozilla.org/MPL/2.0/. #include "world/MemCell.h" #include "application/editoruilayer.h" #include "rendering/renderer.h" +#include "simulation/simulation.h" #include "editor/editorModelSets.hpp" namespace { +// tells why provided text can't be the name of a node, empty text if it can. the scenery parser has to get it back as a single token +std::string node_name_issue(std::string const &Name) +{ + if (Name.find_first_of(" \t\r\n;\"") != std::string::npos) + { + return "a name can't hold spaces, semicolons or quotes"; + } + if (Name.find("//") != std::string::npos || Name.find("/*") != std::string::npos) + { + return "a name can't hold a comment mark"; + } + if (Name == "include") + { + return "this word has a meaning of its own in scenery files"; + } + return {}; +} + // list entry; the selected one is drawn with the accent colour, like selections in the starter bool list_item(char const *Label, bool const Selected) { @@ -322,12 +341,75 @@ void itemproperties_panel::render_body() { ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); } + // name of the node — TAnimModel only + render_name_editor(); // transform editor (position/rotation/scale) — TAnimModel only render_transform_editor(); // group section render_group(); } +// Name of a picked TAnimModel. The editor places decorations without names, as each name takes its share of the time +// the scenery needs to load; this is where an instance gets one, for the events to refer to it. +void itemproperties_panel::render_name_editor() +{ + if (m_node == nullptr) { return; } + if (typeid(*m_node) != typeid(TAnimModel)) { return; } + auto *picked = static_cast(m_node); + // the name of an instance defined by a template, or by a file which can't be rewritten, wouldn't make it to the scenery files + if (false == scene::Layers.editable(picked)) { return; } + + if (m_namednode != m_node || false == m_nameactive) + { + // the field shows the name of the node unless it's being typed in + if (m_namednode != m_node) + { + m_nameissue.clear(); + } + m_namednode = m_node; + std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str()); + } + auto const entered{ImGui::InputTextWithHint("name", "(none)", m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)}; + m_nameactive = ImGui::IsItemActive(); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it"); + } + if (entered || ImGui::IsItemDeactivatedAfterEdit()) + { + // names are lower case, like everything else the scenery parser hands over + std::string name{m_namebuffer}; + for (auto &character : name) + { + if (character >= 'A' && character <= 'Z') + { + character = static_cast(character - 'A' + 'a'); + } + } + if (name == "none") + { + name.clear(); + } + if (name != picked->name()) + { + m_nameissue = node_name_issue(name); + if (m_nameissue.empty() && false == simulation::State.rename_model(picked, name)) + { + m_nameissue = "another model instance goes by this name"; + } + } + else + { + m_nameissue.clear(); + } + m_nameactive = false; + } + if (false == m_nameissue.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", m_nameissue.c_str()); + } +} + // In-place editor for position (double precision), rotation (degrees, 0-360), // and uniform scale (per-axis float, 1.000) of a picked TAnimModel. // Other node subclasses don't expose these knobs through the same API, so the diff --git a/application/editoruipanels.h b/application/editoruipanels.h index b558a069..7da83870 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -69,11 +69,17 @@ class itemproperties_panel : public ui_panel // renders DragFloat3/DragScalarN widgets for position, rotation and scale // of the currently bound TAnimModel; no-op for other node subclasses. void render_transform_editor(); + // renders the field with the name of the currently bound TAnimModel, which gives the instance another name; no-op for other node subclasses + void render_name_editor(); // renders widgets for the parameters of the bound include void render_include(); // members scene::basic_node *m_node{nullptr}; // scene node bound to the panel + scene::basic_node *m_namednode{nullptr}; // scene node whose name is held by the edit buffer + char m_namebuffer[128]{}; // name of the node, as shown and typed in + bool m_nameactive{false}; // the name is being typed in + std::string m_nameissue; // why the name typed in wasn't taken include_selection *m_include{nullptr}; // include of a scenery template bound to the panel scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel std::string m_groupprefix; diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 09770444..7cfd8b14 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -129,6 +129,7 @@ void node_layers::clear() m_context = layer_context(); m_sources.clear(); m_erased.clear(); + m_renamed.clear(); m_shapes.clear(); m_shapeserased = false; m_templates.clear(); @@ -475,6 +476,17 @@ void node_layers::forget(basic_node const *Node) } m_erased.emplace_back(lookup->second.layer, lookup->second.span); m_sources.erase(lookup); + m_renamed.erase(Node); +} + +// indicates specified node was given another name in the editor +void node_layers::renamed(basic_node const *Node) +{ + // NOTE: a node which isn't defined by a scenery file yet is written whole on save, along with its name + if (m_sources.find(Node) != m_sources.end()) + { + m_renamed.emplace(Node); + } } void node_layers::shape(material_handle const Material, source_span const &Span) diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 5a6f249c..f59a37e0 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -259,6 +259,8 @@ class node_layers void track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles = glm::vec3{0.f}, glm::vec3 const &Scale = glm::vec3{1.f}); // indicates specified node was removed from the scene void forget(basic_node const *Node); + // indicates specified node was given another name in the editor, for its definition to get it on save + void renamed(basic_node const *Node); // stores location of a shape defined directly in the layer file being loaded void shape(material_handle const Material, source_span const &Span); std::vector const &shapes() const @@ -373,6 +375,7 @@ class node_layers layer_context m_context; // placement in effect at the current point of scenery load 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::unordered_set m_renamed; // nodes given another name since the load or the last save std::vector m_shapes; bool m_shapeserased{false}; // the binary terrain file holds shapes which the save drops std::set m_templates; // *.inc templates used by the scenery diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 21d8a2be..98da1f12 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -134,6 +134,7 @@ struct model_placement std::optional angles; // complete rotation of the instance glm::vec3 rotation{0.f}; // rotation the loader adds to the angle specified in the definition std::optional scale; // scale factor to put in the definition + std::optional name; // name of the node }; bool about_equal(float const Left, float const Right) @@ -166,6 +167,10 @@ bool patch_model(std::string &Text, model_placement const &Placement, std::strin }; std::string additions; // blocks to insert ahead of the closing keyword + if (Placement.name) + { + replace(3, *Placement.name); + } if (Placement.location) { replace(5, number(Placement.location->x)); @@ -693,7 +698,8 @@ save_result node_layers::save(std::vector const &Rootstatements) auto const moved{glm::distance(location, source.location) > 1e-4}; auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))}; auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))}; - if (false == (moved || rotated || scaled)) + auto const renamed{Model != nullptr && m_renamed.find(Node) != m_renamed.end()}; + if (false == (moved || rotated || scaled || renamed)) { return true; } @@ -725,6 +731,10 @@ save_result node_layers::save(std::vector const &Rootstatements) { placement.scale = scale / source.context.scale; } + if (renamed) + { + placement.name = (Node->name().empty() ? "none" : Node->name()); + } patched = patch_model(text, placement, error); } else @@ -1614,6 +1624,7 @@ save_result node_layers::save(std::vector const &Rootstatements) included.dead = included.dead || included.removed; } m_erased.clear(); + m_renamed.clear(); // the shapes of the rewritten files are somewhere else in them now m_shapes.erase(std::remove_if(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return std::find(std::begin(composed), std::end(composed), Shape.layer) != std::end(composed); }), std::end(m_shapes)); for (auto *path : savedpaths) diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index a5d5ad29..035a4f37 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -557,6 +557,15 @@ void state_manager::delete_model(TAnimModel *model) { Instances.purge(model); } +bool state_manager::rename_model(TAnimModel *model, std::string const &name) { + if (model == nullptr || false == Instances.rename(model, name)) { + return false; + } + model->m_name = (name == "none" ? std::string{} : name); + scene::Layers.renamed(model); + return true; +} + void state_manager::delete_eventlauncher(TEventLauncher *launcher) { launcher->dRadius = 0.0f; // disable it } diff --git a/simulation/simulation.h b/simulation/simulation.h index b36d70a0..89337583 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -54,6 +54,9 @@ public: // delete TAnimModel instance void delete_model(TAnimModel *model); + // gives TAnimModel instance another name; empty text leaves it without one. returns: false if another instance uses the name + bool + rename_model(TAnimModel *model, std::string const &name); // delete TEventLauncher instance void delete_eventlauncher(TEventLauncher *launcher); diff --git a/utilities/Names.h b/utilities/Names.h index 0533c696..52d438f6 100644 --- a/utilities/Names.h +++ b/utilities/Names.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include template class basic_table { @@ -31,7 +32,7 @@ public: } auto const itemhandle { m_items.size() - 1 }; // add item name to the map - auto mapping = m_itemmap.emplace( itemname, itemhandle ); + auto mapping = m_itemmap.emplace( std::move( itemname ), itemhandle ); if( true == mapping.second ) { return true; } @@ -42,6 +43,33 @@ public: { return insert(Item, Item->name()); } + // changes the name specified item is found by; empty text or "none" leaves the item without one. + // NOTE: the name held by the item itself is for the caller to change afterwards. + // returns: false if the name is in use by another item or the item isn't in the collection, true otherwise + bool + rename( Type_ const *Item, std::string const &Name ) { + auto const named { false == Name.empty() && Name != "none" }; + if( named ) { + auto const taken { m_itemmap.find( Name ) }; + if( taken != m_itemmap.end() + && m_items[ taken->second ] != nullptr + && m_items[ taken->second ] != Item ) { + return false; } } + // the current name leads to the item unless the item has none, or shares it with an item added later + auto itemhandle { m_items.size() }; + auto const current { m_itemmap.find( Item->name() ) }; + if( current != m_itemmap.end() + && m_items[ current->second ] == Item ) { + itemhandle = current->second; + m_itemmap.erase( current ); } + else { + auto const lookup { std::find( m_items.begin(), m_items.end(), Item ) }; + if( lookup == m_items.end() ) { + return false; } + itemhandle = static_cast( std::distance( m_items.begin(), lookup ) ); } + if( named ) { + m_itemmap[ Name ] = itemhandle; } + return true; } void purge (std::string const &Name) { auto lookup = m_itemmap.find( Name );