diff --git a/application/editormode.cpp b/application/editormode.cpp index 86b51e97..ce22371d 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -675,10 +675,8 @@ void editor_mode::undo_last() 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); + restore_include(snap); g_redo.push_back(std::move(snap)); - m_instance = 0; return; } @@ -766,9 +764,8 @@ void editor_mode::redo_last() if (snap.instance != 0) { - toggle_include(snap.instance); + restore_include(snap); m_history.push_back(std::move(snap)); - m_instance = 0; return; } @@ -926,8 +923,27 @@ bool editor_mode::update() } if (m_instance != 0 && false == scene::Layers.removable(m_instance)) { - m_instance = 0; + select_include(0); } + if (m_include.changed) + { + // a parameter of the selected include was changed in the node properties + m_include.changed = false; + apply_include(); + } + if (false == m_include.active && false == m_gizmo_using) + { + m_include_gesture = false; + } + // models replaced after changes of includes are destroyed once the renderer can't have them among its pick candidates + for (auto &retired : m_retired) + { + if (--retired.second <= 0) + { + simulation::Instances.purge(retired.first); + } + } + m_retired.erase(std::remove_if(m_retired.begin(), m_retired.end(), [](std::pair const &Retired) { return Retired.second <= 0; }), m_retired.end()); simulation::Region->update_sounds(); audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); @@ -1615,6 +1631,97 @@ void editor_mode::toggle_include(scene::instance_handle const Instance) ui()->set_status("Include of \"" + *included.file + "\" " + (included.removed ? "removed." : "restored.")); } +void editor_mode::restore_include(EditorSnapshot &Snapshot) +{ + if (Snapshot.serialized.empty()) + { + // the include was placed or removed; either is taken back by flipping its removal + toggle_include(Snapshot.instance); + select_include(0); + return; + } + // parameters of the include were changed. the snapshot holds the directive to go back to, and leaves with the current one + auto current = scene::Layers.directive(Snapshot.instance); + if (current.empty() || scene::Layers.instance(Snapshot.instance).dead) + { + ui()->set_status("Include of \"" + Snapshot.node_name + "\" is gone from the scenery, the change can't be taken back.", true); + return; + } + set_include_directive(Snapshot.instance, Snapshot.serialized); + Snapshot.serialized = std::move(current); + select_include(scene::Layers.removable(Snapshot.instance) ? Snapshot.instance : 0); +} + +void editor_mode::select_include(scene::instance_handle const Instance) +{ + m_instance = (scene::Layers.tracked(Instance) ? Instance : 0); + m_include = include_selection{}; + m_include_gesture = false; + if (m_instance == 0) + { + ui()->set_include(nullptr); + return; + } + auto const &included = scene::Layers.instance(m_instance); + m_include.instance = m_instance; + m_include.target = *included.file; + // NOTE: a template without a usable description is still presented, with its parameters unnamed + std::string error; + editor_includes::load(*included.file, m_include.info, error); + if (false == editor_includes::parse_directive(scene::Layers.directive(m_instance), m_include.target, m_include.values)) + { + m_include.values.clear(); + m_include.issue = "The directive of the include can't be read from the scenery file, its parameters can't be changed."; + } + // with a rotation or a scale in effect the parameters don't translate to a location in the scene in a single way, + // see place_include() + m_include.placement = (included.context.rotation == glm::vec3{0.f} && included.context.scale == glm::vec3{1.f}); + ui()->set_include(&m_include); +} + +void editor_mode::set_include_directive(scene::instance_handle const Instance, std::string const &Directive) +{ + if (false == scene::Layers.modify(Instance, Directive)) + return; + std::vector retired; + simulation::State.rebuild_include(Instance, retired); + // the pick of the renderer is resolved a few frames after it's requested, and can still deliver one of these + for (auto *model : retired) + m_retired.emplace_back(model, 120); +} + +void editor_mode::apply_include() +{ + if (m_instance == 0 || false == m_include.issue.empty()) + return; + for (auto const &value : m_include.values) + { + if (value == "end" || value.find_first_of("\"\r\n") != std::string::npos) + { + // either would cut the directive short + ui()->set_status("A parameter of an include can't be \"end\", or have a quotation mark in it.", true); + select_include(m_instance); // back to the values of the directive + return; + } + } + auto before = scene::Layers.directive(m_instance); + auto const directive = editor_includes::compose_directive(m_include.target, m_include.values); + if (directive == before) + return; + if (false == m_include_gesture) + { + // a single undo step for the whole drag + EditorSnapshot snap; + snap.instance = m_instance; + snap.node_name = *scene::Layers.instance(m_instance).file; + snap.serialized = std::move(before); + m_history.push_back(std::move(snap)); + g_redo.clear(); + m_include_gesture = true; + } + set_include_directive(m_instance, directive); +} + void editor_mode::place_include(std::string const &File, int const RotationMode, float const FixedRotation) { if (scene::Layers.empty()) @@ -1670,6 +1777,7 @@ void editor_mode::place_include(std::string const &File, int const RotationMode, snap.node_name = File; m_history.push_back(std::move(snap)); g_redo.clear(); + select_include(instance); ui()->set_status( "\"" + info.name + "\" placed in layer \"" + scene::Layers.layer(layer).name + "\": " + std::to_string(preview.first) + " model(s) shown" @@ -2097,7 +2205,15 @@ void editor_mode::render_gizmo_options() if (m_gizmo_snap < 0.0f) m_gizmo_snap = 0.0f; } - if (!m_node) + if (m_instance != 0) + { + auto const located = editor_includes::parameter_with_role(m_include.info, "pos.x") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.y") != 0 || + editor_includes::parameter_with_role(m_include.info, "pos.z") != 0; + ImGui::TextDisabled("%s", false == located ? "Include selected, no position parameters described" : + false == m_include.placement ? "Include selected, no gizmo under rotate or scale" : + "Include selected, axes as its template allows"); + } + else if (!m_node) ImGui::TextDisabled("No node selected"); } @@ -2110,7 +2226,7 @@ void editor_mode::render_gizmo() return; } - if (!m_node) + if (!m_node && m_instance == 0) { m_gizmo_using = false; return; @@ -2134,6 +2250,12 @@ void editor_mode::render_gizmo() float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; glm::mat4 const projection = glm::perspective(fovy, aspect, 0.1f, 10000.0f); + if (!m_node) + { + render_include_gizmo(view, projection, camerapos); + return; + } + // rotation/scale are only meaningful for instanced models; other node types translate only TAnimModel *model = dynamic_cast(m_node); @@ -2211,6 +2333,103 @@ void editor_mode::render_gizmo() } } +void editor_mode::render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos) +{ + m_gizmo_using = false; + if (false == m_include.placement || false == m_include.issue.empty()) + return; + + // parameters of the template the gizmo can drive, as indices of their values; -1 where the template has none + static char const *const positionroles[] = {"pos.x", "pos.y", "pos.z"}; + static char const *const rotationroles[] = {"rot.x", "rot.y", "rot.z"}; + auto const numeric_parameter = [this](char const *Role, double &Value) { + auto const index = editor_includes::parameter_with_role(m_include.info, Role) - 1; + if (index < 0 || index >= static_cast(m_include.values.size())) + return -1; + auto const &text = m_include.values[index]; + char *end = nullptr; + auto const number = std::strtod(text.c_str(), &end); + if (text.empty() || *end != '\0') + return -1; + Value = number; + return index; + }; + int position[3], rotation[3]; + double location[3] = {0.0, 0.0, 0.0}, angles[3] = {0.0, 0.0, 0.0}; + auto positions = 0, rotations = 0; + for (auto axis = 0; axis < 3; ++axis) + { + position[axis] = numeric_parameter(positionroles[axis], location[axis]); + rotation[axis] = numeric_parameter(rotationroles[axis], angles[axis]); + positions += (position[axis] >= 0 ? 1 : 0); + rotations += (rotation[axis] >= 0 ? 1 : 0); + } + if (positions == 0) + return; // nothing tells where the include is + + // only the axes the template has parameters for can be manipulated + auto operation = 0; + for (auto axis = 0; axis < 3; ++axis) + { + if (m_gizmo_op == gizmo_operation::translate && position[axis] >= 0) + operation |= (ImGuizmo::TRANSLATE_X << axis); + else if (m_gizmo_op == gizmo_operation::rotate && rotation[axis] >= 0) + operation |= (ImGuizmo::ROTATE_X << axis); + } + if (operation == 0) + return; + auto const translating = (m_gizmo_op == gizmo_operation::translate); + + // the parameters are relative to the origin in effect at the directive + auto const &offset = scene::Layers.instance(m_instance).context.offset; + glm::vec3 const relativepos = glm::vec3(offset + glm::dvec3{location[0], location[1], location[2]} - Camerapos); + float const translation[3] = {relativepos.x, relativepos.y, relativepos.z}; + float const rotationangles[3] = {static_cast(angles[0]), static_cast(angles[1]), static_cast(angles[2])}; + float const scale[3] = {1.0f, 1.0f, 1.0f}; + glm::mat4 matrix(1.0f); + ImGuizmo::RecomposeMatrixFromComponents(translation, rotationangles, scale, glm::value_ptr(matrix)); + + // moving along the axes of the include would need all three of them + ImGuizmo::MODE const mode = (m_gizmo_local && (false == translating || positions == 3)) ? ImGuizmo::LOCAL : ImGuizmo::WORLD; + glm::vec3 const snapvalue(translating ? m_gizmo_snap : 5.0f); + float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; + + ImGuizmo::Manipulate(glm::value_ptr(View), glm::value_ptr(Projection), static_cast(operation), mode, glm::value_ptr(matrix), nullptr, snap); + + if (false == ImGuizmo::IsUsing()) + return; + m_gizmo_using = true; + + float newtranslation[3], newrotation[3], newscale[3]; + ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(matrix), newtranslation, newrotation, newscale); + if (translating) + { + glm::dvec3 const newlocation = Camerapos + glm::dvec3{newtranslation[0], newtranslation[1], newtranslation[2]} - offset; + for (auto axis = 0; axis < 3; ++axis) + { + if (position[axis] >= 0) + m_include.values[position[axis]] = editor_includes::number(newlocation[axis]); + } + } + else + { + if (rotations == 1) + { + // the decomposition spreads a turn of more than a quarter over all three angles; + // with a single axis to turn around the angle is taken straight from the matrix + newrotation[0] = glm::degrees(std::atan2(matrix[1][2], matrix[1][1])); + newrotation[1] = glm::degrees(std::atan2(-matrix[0][2], matrix[0][0])); + newrotation[2] = glm::degrees(std::atan2(matrix[0][1], matrix[0][0])); + } + for (auto axis = 0; axis < 3; ++axis) + { + if (rotation[axis] >= 0) + m_include.values[rotation[axis]] = editor_includes::number(clamp_circular(newrotation[axis])); + } + } + apply_include(); +} + void editor_mode::update_camera(double const Deltatime) { Camera.Update(); @@ -2294,7 +2513,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; + select_include(0); m_gizmo_using = false; ui()->set_node(nullptr); @@ -2400,7 +2619,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in 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; + select_include(0); break; } if (is_press(Action)) @@ -2526,7 +2745,7 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; m_node = nullptr; - m_instance = 0; + select_include(0); // 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 @@ -2536,8 +2755,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const 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."); + select_include(node->m_instance); + ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" selected. Its parameters are in the node properties, Del removes it from the scenery."); node = nullptr; } else if (node->from_template() || false == scene::Layers.editable(node, &reason)) diff --git a/application/editormode.h b/application/editormode.h index f5248c71..edd35ea7 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -129,6 +129,9 @@ class editor_mode : public application_mode 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 + include_selection m_include; // parameters of the selected include, changed with the gizmo and in the node properties + bool m_include_gesture{false}; // a change of the selected include is under way, and has its undo snapshot already + std::vector> m_retired; // models replaced after changes of includes, with the frames left until they're destroyed 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 @@ -173,6 +176,16 @@ class editor_mode : public application_mode 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); + // makes specified include the selected one, 0 clears the selection + void select_include(scene::instance_handle Instance); + // writes parameter values of the selected include to its directive, and shows the outcome in the scene + void apply_include(); + // replaces directive of specified include, and the models shown for it + void set_include_directive(scene::instance_handle Instance, std::string const &Directive); + // undo and redo of an include snapshot + void restore_include(EditorSnapshot &Snapshot); + // gizmo for the selected include, limited to what the parameters of its template can express + void render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos); // 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 70b6e0e1..42239690 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -164,6 +164,11 @@ void editor_ui::set_node(scene::basic_node *Node) m_node = Node; } +void editor_ui::set_include(include_selection *Include) +{ + m_itempropertiespanel.include(Include); +} + void editor_ui::add_node_template(const std::string &desc) { m_nodebankpanel.add_template(desc); diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 3f24d7c4..bc9edbe1 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -29,6 +29,8 @@ class editor_ui : public ui_layer // updates state of UI elements void update() override; void set_node(scene::basic_node *Node); + // include of a scenery template to show in the node properties in place of a node; nullptr if there's none + void set_include(include_selection *Include); void add_node_template(const std::string &desc); float rot_val(); bool rot_from_last(); diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index b93b5b0f..fee74f2f 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -312,6 +312,11 @@ void itemproperties_panel::render() void itemproperties_panel::render_body() { + if (m_include != nullptr) + { + render_include(); + return; + } // header section for (auto const &line : text_lines) { @@ -1187,6 +1192,74 @@ ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f}; } // namespace +void itemproperties_panel::render_include() +{ + auto &include{*m_include}; + include.active = false; + + ImGui::Text("include: %s", Bezogonkow(include.target).c_str()); + if (false == include.info.name.empty()) + { + ImGui::TextDisabled("%s", include.info.name.c_str()); + } + if (false == include.issue.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(include.issue).c_str()); + ImGui::PopTextWrapPos(); + return; + } + if (include.values.empty()) + { + ImGui::TextDisabled("The template takes no parameters."); + return; + } + + ImGui::PushItemWidth(ImGui::GetFontSize() * 14.0f); + for (std::size_t idx = 0; idx < include.values.size(); ++idx) + { + auto const id{static_cast(idx + 1)}; + auto const lookup{std::find_if(std::begin(include.info.parameters), std::end(include.info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; + auto const described{lookup != std::end(include.info.parameters)}; + auto const role{described ? lookup->role : std::string{"free"}}; + // the label from the description, or what the description tells about the parameter + auto const label{described && false == lookup->label.empty() ? lookup->label : role != "free" ? role : "(p" + std::to_string(id) + ")"}; + auto &value{include.values[idx]}; + + ImGui::PushID(id); + char *end{nullptr}; + auto number{std::strtod(value.c_str(), &end)}; + if ((role.starts_with("pos.") || role.starts_with("rot.") || role == "number") && false == value.empty() && *end == '\0') + { + if (ImGui::DragScalar(label.c_str(), ImGuiDataType_Double, &number, role.starts_with("pos.") ? 0.05f : 0.5f, nullptr, nullptr, "%.3f")) + { + value = editor_includes::number(number); + include.changed = true; + } + } + else + { + // applied once the field is left, so the template isn't processed with a half-typed value + input_text(label.c_str(), value); + if (ImGui::IsItemDeactivatedAfterEdit()) + { + include.changed = true; + } + } + include.active |= ImGui::IsItemActive(); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("(p%d), %s", id, role.c_str()); + } + ImGui::PopID(); + } + ImGui::PopItemWidth(); + + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again."); + ImGui::PopStyleColor(); +} + void layers_panel::report(std::string const &Status, bool const Error) { status = Status; diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 099dc85f..9d48ccce 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -29,6 +29,20 @@ struct item_properties { changeable rotation {}; }; */ +// include of a scenery template selected in the editor, with the values its directive passes to the template. +// owned by the editor mode; the node properties let the user change the values +struct include_selection +{ + scene::instance_handle instance{0}; + std::string target; // name of the template, the way the directive spells it + include_info info; // description of the template + std::vector values; // value of each parameter in turn + bool placement{false}; // the position and rotation parameters can be driven by the gizmo + std::string issue; // why the values can't be changed, if they can't + bool changed{false}; // set by the UI when a value was changed, cleared by the editor mode once the change is applied + bool active{false}; // the UI is in the middle of changing a value +}; + class itemproperties_panel : public ui_panel { @@ -39,6 +53,11 @@ class itemproperties_panel : public ui_panel // position/rotation/scale of TAnimModel nodes in place. Other node types // are still treated as read-only. void update(scene::basic_node *Node); + // binds include of a scenery template to the panel, shown in place of a node; nullptr unbinds + void include(include_selection *Include) + { + m_include = Include; + } void render() override; // panel content without the window, also drawn inside the toolset window void render_body(); @@ -50,9 +69,12 @@ 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 widgets for the parameters of the bound include + void render_include(); // members scene::basic_node *m_node{nullptr}; // scene node bound to the panel + 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; std::vector m_grouplines; diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp index 48c14062..46116f9e 100644 --- a/editor/editorIncludeInfo.cpp +++ b/editor/editorIncludeInfo.cpp @@ -543,15 +543,99 @@ bool complete(include_info const &Info, int const Parameters, std::string *Issue return issue.empty(); } +std::string number(double const Value) +{ + std::ostringstream converter; + converter.imbue(std::locale::classic()); + converter << std::fixed << std::setprecision(3) << Value; + return converter.str(); +} + +int parameter_with_role(include_info const &Info, std::string const &Role) +{ + auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [&](include_parameter const &Parameter) { return Parameter.role == Role; })}; + return lookup != std::end(Info.parameters) ? lookup->id : 0; +} + +bool parse_directive(std::string const &Directive, std::string &File, std::vector &Values) +{ + // the same rules the scenery parser goes by, except the text is taken as it is written + std::vector tokens; + std::string token; + auto const flush = [&]() { + if (false == token.empty()) + { + tokens.emplace_back(token); + token.clear(); + } + }; + for (std::size_t idx = 0; idx < Directive.size(); ++idx) + { + auto const character{Directive[idx]}; + if (character == '\"') + { + flush(); + auto const end{Directive.find('\"', idx + 1)}; + if (end == std::string::npos) + { + return false; + } + tokens.emplace_back(Directive, idx + 1, end - idx - 1); + idx = end; + } + else if (Directive.compare(idx, 2, "//") == 0) + { + flush(); + idx = Directive.find('\n', idx); + if (idx == std::string::npos) + { + break; + } + } + else if (Directive.compare(idx, 2, "/*") == 0) + { + flush(); + idx = Directive.find("*/", idx + 2); + if (idx == std::string::npos) + { + break; + } + ++idx; + } + else if (character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == ';') + { + flush(); + } + else + { + token += character; + } + } + flush(); + if (tokens.size() < 3 || ToLower(tokens.front()) != "include" || tokens.back() != "end") + { + return false; + } + File = tokens[1]; + Values.assign(std::begin(tokens) + 2, std::end(tokens) - 1); + return true; +} + +std::string compose_directive(std::string const &File, std::vector const &Values) +{ + auto const quoted = [](std::string const &Text) { return Text.find_first_of(" \t;") != std::string::npos ? '\"' + Text + '\"' : Text; }; + std::string text{"include " + quoted(File)}; + for (auto const &value : Values) + { + // a parameter can't be left out, as the ones after it would take its place + text += ' ' + (value.empty() ? std::string{"none"} : quoted(value)); + } + return text + " end"; +} + 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}; + std::vector values; 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; })}; @@ -585,14 +669,9 @@ std::string directive(std::string const &File, include_info const &Info, int con 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); + values.emplace_back(value); } - return text + " end"; + return compose_directive(File, values); } } // namespace editor_includes diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp index 1273a049..90241406 100644 --- a/editor/editorIncludeInfo.hpp +++ b/editor/editorIncludeInfo.hpp @@ -68,6 +68,16 @@ bool complete(include_info const &Info, int Parameters, std::string *Issue = nul // 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); +// splits include directive into the name of the included file and the values of the parameters, both as they're +// written. returns: false if the text isn't a complete include directive +bool parse_directive(std::string const &Directive, std::string &File, std::vector &Values); +// builds include directive out of the name of the included file and the values of the parameters +std::string compose_directive(std::string const &File, std::vector const &Values); +// text of a number passed as a parameter value +std::string number(double Value); +// number of the parameter with specified role, 0 if the description has none +int parameter_with_role(include_info const &Info, std::string const &Role); + // marks node bank template which stands for a scenery template rather than for definition of a single node extern std::string const directive_mark; diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index a3b3f51e..99015442 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -210,8 +210,8 @@ void node_layers::visible(layer_handle Layer, bool const Visible) } if (Visible) { - // nodes of a removed include stay out of sight - if (Node->m_layerhidden && (false == tracked(Node->m_instance) || false == instance(Node->m_instance).removed)) + // nodes of a removed include stay out of sight, and so do the ones the editor replaced with its own + if (Node->m_layerhidden && (false == tracked(Node->m_instance) || (false == instance(Node->m_instance).removed && false == stale(Node)))) { Node->visible(true); Node->m_layerhidden = false; @@ -613,7 +613,7 @@ bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::strin { // 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; + return false == Instance.dead && false == Instance.removed && false == Instance.span.valid() && resolve(Instance.layer) == Source; })}; if (unsaved) { @@ -695,6 +695,7 @@ void node_layers::instance_begin(std::string const &File, source_span const &Spa included.layer = handle(); included.file = &(*m_templates.emplace(File).first); included.span = Span; + included.context = m_context; m_instance = static_cast(m_instances.size()); } @@ -741,7 +742,7 @@ void node_layers::removed(instance_handle Instance, bool 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) + if (Node == nullptr || Node->m_instance != Instance || (Item == layer_item::model && stale(Node))) { return; } @@ -791,6 +792,67 @@ instance_handle node_layers::place(layer_handle Layer, std::string const &File, return static_cast(m_instances.size()); } +// text of the directive of specified include, as changed in the editor or as it stands in the scenery file +std::string node_layers::directive(instance_handle const Instance) const +{ + if (false == tracked(Instance)) + { + return {}; + } + auto const &included{instance(Instance)}; + if (false == included.directive.empty() || false == included.span.valid() || false == valid(included.layer)) + { + return included.directive; + } + std::string text(static_cast(included.span.end - included.span.begin), '\0'); + std::ifstream input{path(included.layer), std::ios_base::binary}; + if (false == input.is_open() || false == input.seekg(included.span.begin).good() || false == input.read(text.data(), static_cast(text.size())).good()) + { + return {}; + } + return text; +} + +// replaces the directive of specified include, the scenery file receives it on save +bool node_layers::modify(instance_handle const Instance, std::string const &Directive) +{ + if (false == tracked(Instance) || m_instances[Instance - 1].dead || Directive.empty()) + { + return false; + } + m_instances[Instance - 1].directive = Directive; + return true; +} + +// takes the models specified include was loaded with out of the scene, once the editor shows its own in their place +void node_layers::rebuilt(instance_handle const Instance) +{ + if (false == tracked(Instance) || m_instances[Instance - 1].rebuilt) + { + return; + } + auto const removed{m_instances[Instance - 1].removed}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr || model->m_instance != Instance || model->m_preview) + { + continue; + } + // NOTE: the models themselves are left alone, events of the scenery can refer to them + simulation::Region->erase(model); + if (false == removed) + { + count(model->layer(), layer_item::model, -1); + } + if (model->visible()) + { + model->visible(false); + model->m_layerhidden = true; + } + } + m_instances[Instance - 1].rebuilt = true; +} + } // namespace scene //--------------------------------------------------------------------------- diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 44661fc5..eacc6421 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -100,10 +100,11 @@ 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 + std::string directive; // text of the directive, for include placed or changed in the editor and not saved yet + layer_context context; // placement in effect at the directive bool removed{false}; // the include is to be dropped from the scenery bool dead{false}; // removal of the include was saved + bool rebuilt{false}; // models the include was loaded with gave way to the ones made by the editor }; // marks nodes defined by a template which was included in a way the editor doesn't keep track of @@ -277,6 +278,18 @@ class node_layers // 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); + // text of the directive of specified include, as changed in the editor or as it stands in the scenery file. + // returns: empty text if the directive can't be read + std::string directive(instance_handle Instance) const; + // replaces the directive of specified include, the scenery file receives it on save. NOTE: the nodes aren't touched + bool modify(instance_handle Instance, std::string const &Directive); + // takes the models specified include was loaded with out of the scene, once the editor shows its own in their place + void rebuilt(instance_handle Instance); + // true if specified node was replaced by the editor with a node showing the current state of its include + bool stale(basic_node const *Node) const + { + return tracked(Node->m_instance) && instance(Node->m_instance).rebuilt && false == Node->m_preview; + } // true if specified layer is included, directly or not, by the other one bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index f56c5c11..3a4071a2 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -275,6 +275,7 @@ struct node_layers::file_patch std::size_t length{0}; // size of that definition basic_node const *node{nullptr}; // node defined by the replacement layer_handle site{null_handle}; // layer included by the directive in the replacement + instance_handle instance{0}; // include of a template made by the directive in the replacement layer_handle inlined{null_handle}; // layer whose content replaces the range }; std::vector edits; @@ -460,6 +461,10 @@ bool node_layers::compose(save_state &State, layer_handle const Layer, compositi { Output.sites.push_back({edit.site, span}); } + if (edit.instance != 0) + { + Output.instances.push_back({edit.instance, span}); + } Output.text.append(edit.text, edit.lead - lead, std::string::npos); } cursor = end; @@ -692,6 +697,17 @@ save_result node_layers::save(std::vector const &Rootstatements) edit.end = included.span.end; state.patches[included.layer].edits.emplace_back(std::move(edit)); } + else if (false == included.directive.empty()) + { + // changed in the editor, the new directive takes the place of the one in the file + file_patch::edit edit; + edit.begin = included.span.begin; + edit.end = included.span.end; + edit.length = included.directive.size(); + edit.text = included.directive; + edit.instance = static_cast(idx + 1); + state.patches[included.layer].edits.emplace_back(std::move(edit)); + } } else if (false == included.removed && false == included.directive.empty()) { @@ -840,7 +856,7 @@ save_result node_layers::save(std::vector const &Rootstatements) 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) + if (false == included.dead && false == included.removed && included.directive.empty() && included.span.valid() && state.content.count(included.layer) != 0) { state.instances[included.layer].emplace_back(static_cast(idx + 1)); } diff --git a/scene/scenenode.h b/scene/scenenode.h index 8de0e060..df60187f 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -385,6 +385,7 @@ public: std::string m_name; bool m_dirty { false }; bool m_layerhidden { false }; // the node was visible until the editor hid its scenery layer, or the include which defines it + bool m_preview { false }; // the node was made by the editor to show an include it placed or changed, and is replaced when the include changes 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 diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index e50eff37..5f04c611 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -497,6 +497,51 @@ std::pair state_manager::preview_include(std::string const &Directive, return m_serializer.preview_include(Directive, Context, Layer, Instance); } +std::pair state_manager::rebuild_include(scene::instance_handle const Instance, std::vector &Retired) { + if (false == scene::Layers.tracked(Instance)) { + return { 0, 0 }; + } + auto const &included { scene::Layers.instance(Instance) }; + // the models the include was loaded with don't match it anymore. + // NOTE: done ahead of retiring the models made by the editor, which are told from the loaded ones by their preview flag + scene::Layers.rebuilt(Instance); + for (auto *model : Instances.sequence()) { + if (model == nullptr || model->m_instance != Instance || false == model->m_preview) { + continue; + } + if (false == included.removed) { + scene::Layers.count(model->layer(), scene::layer_item::model, -1); + } + Region->erase(model); + scene::Hierarchy.erase(model->uuid.to_string()); + model->visible(false); + model->m_preview = false; + Retired.emplace_back(model); + } + if (included.dead) { + return { 0, 0 }; + } + auto const layer { scene::Layers.resolve(included.layer) }; + auto const result { m_serializer.preview_include(scene::Layers.directive(Instance), included.context, layer, Instance) }; + // what's shown for an include which is out of sight at the moment goes out of sight as well + auto const layerhidden { scene::Layers.valid(layer) && false == scene::Layers.layer(layer).visible }; + if (included.removed || layerhidden) { + for (auto *model : Instances.sequence()) { + if (model == nullptr || model->m_instance != Instance || false == model->m_preview) { + continue; + } + if (included.removed) { + scene::Layers.count(model->layer(), scene::layer_item::model, -1); + } + if (model->visible()) { + model->visible(false); + model->m_layerhidden = true; + } + } + } + return result; +} + 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 fe5fc87b..b75056ad 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -46,6 +46,11 @@ public: // 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); + // replaces models shown for specified include with the ones defined by its current directive. Retired: receives + // the replaced models made by the editor. they're out of the scene already, but the renderer can still hold on + // to them for a few frames; pass them to Instances.purge() afterwards + std::pair + rebuild_include(scene::instance_handle Instance, std::vector &Retired); // delete TAnimModel instance void delete_model(TAnimModel *model); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index ae0841af..459e7755 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -1451,12 +1451,9 @@ std::pair state_serializer::preview_include(std::string const &Directi 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); - } - } + // NOTE: unlike for a model loaded with the scenery, smoke sources of the model aren't set up. + // they'd be left with a dangling owner when the model is replaced after a change of the include + instance->m_preview = true; simulation::Instances.insert(instance); simulation::Region->insert(instance); scene::Hierarchy[instance->uuid.to_string()] = instance; diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index 2dd64307..2a39c934 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -46,7 +46,7 @@ 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. + // creates model instances defined by an include directive placed or changed in the scenery editor, to show the outcome. // 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);