mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-18 02:09:18 +02:00
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
This commit is contained in:
@@ -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<TAnimModel *>(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<std::size_t>(LocalRandom(0.0, static_cast<double>(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<float>(LocalRandom(scalemin, scalemax)));
|
||||
|
||||
@@ -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<TAnimModel *>(m_node);
|
||||
// the name of an instance defined by a template, or by a file which can't be rewritten, wouldn't make it to the scenery files
|
||||
if (false == scene::Layers.editable(picked)) { return; }
|
||||
|
||||
if (m_namednode != m_node || false == m_nameactive)
|
||||
{
|
||||
// the field shows the name of the node unless it's being typed in
|
||||
if (m_namednode != m_node)
|
||||
{
|
||||
m_nameissue.clear();
|
||||
}
|
||||
m_namednode = m_node;
|
||||
std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str());
|
||||
}
|
||||
auto const entered{ImGui::InputTextWithHint("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<char>(character - 'A' + 'a');
|
||||
}
|
||||
}
|
||||
if (name == "none")
|
||||
{
|
||||
name.clear();
|
||||
}
|
||||
if (name != picked->name())
|
||||
{
|
||||
m_nameissue = node_name_issue(name);
|
||||
if (m_nameissue.empty() && false == simulation::State.rename_model(picked, name))
|
||||
{
|
||||
m_nameissue = "another model instance goes by this name";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_nameissue.clear();
|
||||
}
|
||||
m_nameactive = false;
|
||||
}
|
||||
if (false == m_nameissue.empty())
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", m_nameissue.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// In-place editor for position (double precision), rotation (degrees, 0-360),
|
||||
// and uniform scale (per-axis float, 1.000) of a picked TAnimModel.
|
||||
// Other node subclasses don't expose these knobs through the same API, so the
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<shape_source> 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<basic_node const *, node_source> m_sources; // origins of nodes which can be rewritten on save
|
||||
std::vector<std::pair<layer_handle, source_span>> m_erased; // definitions of nodes deleted since the load or the last save
|
||||
std::unordered_set<basic_node const *> m_renamed; // nodes given another name since the load or the last save
|
||||
std::vector<shape_source> m_shapes;
|
||||
bool m_shapeserased{false}; // the binary terrain file holds shapes which the save drops
|
||||
std::set<std::string> m_templates; // *.inc templates used by the scenery
|
||||
|
||||
@@ -134,6 +134,7 @@ struct model_placement
|
||||
std::optional<glm::vec3> angles; // complete rotation of the instance
|
||||
glm::vec3 rotation{0.f}; // rotation the loader adds to the angle specified in the definition
|
||||
std::optional<glm::vec3> scale; // scale factor to put in the definition
|
||||
std::optional<std::string> 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<std::string> 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<std::string> 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<std::string> 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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
template <typename Type_>
|
||||
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::size_t>( 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 );
|
||||
|
||||
Reference in New Issue
Block a user