diff --git a/application/editormode.cpp b/application/editormode.cpp index f5b6d716..6894ba1e 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -476,6 +476,7 @@ editor_mode::editor_mode() { // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); ui()->set_gizmo_options([this]() { render_gizmo_options(); }); + ui()->set_array_options([this]() { render_array(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); ui()->set_menu_options([this]() { render_object_menu(); @@ -873,6 +874,11 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) { if (s.node_ptr == node) s.node_ptr = nullptr; + for (auto © : s.copies) + { + if (copy.model == node) + copy.model = nullptr; + } } for (auto &s : g_redo) @@ -883,6 +889,10 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) // deleted nodes also drop out of the "undo last fill" set m_fill_last.erase(std::remove(m_fill_last.begin(), m_fill_last.end(), node), m_fill_last.end()); + + // an array no longer follows its settings once the model it was made of is gone + if (m_array.base == node) + m_array.base = nullptr; } void editor_mode::undo_last() @@ -912,6 +922,12 @@ void editor_mode::undo_last() return; } + if (snap.action == EditorSnapshot::Action::Array) + { + restore_array_snapshot(std::move(snap), g_redo, true); + return; + } + if (snap.action == EditorSnapshot::Action::Delete) { // undo delete -> recreate model @@ -1016,6 +1032,12 @@ void editor_mode::redo_last() return; } + if (snap.action == EditorSnapshot::Action::Array) + { + restore_array_snapshot(std::move(snap), m_history, false); + return; + } + // handle delete redo (re-delete) separately if (snap.action == EditorSnapshot::Action::Delete) { @@ -1272,6 +1294,9 @@ bool editor_mode::update() draw_road_overlay(); render_road_window(); + // --- array: the one made last is kept in line with its settings --- + update_array(); + // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) draw_area_fill_outline(); @@ -2841,6 +2866,360 @@ void editor_mode::render_area_fill() ImGui::TextUnformatted(m_fill_status.c_str()); } +glm::vec3 editor_mode::array_size(TAnimModel const &Base) +{ + auto *model = Base.Model(); + if (model == nullptr) + return glm::vec3{0.0f}; + if (m_array.sized == model) + return m_array.size; + // the triangles the other tools take for the ground, left where the model has them + std::vector triangles; + gather_submodel_triangles(model->Root, glm::dmat4{1.0}, triangles); + if (triangles.empty()) + return glm::vec3{0.0f}; // not loaded yet, or nothing to it; it's asked about again the next time + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + for (auto const &triangle : triangles) + { + for (auto const &vertex : triangle) + { + low = glm::min(low, vertex); + high = glm::max(high, vertex); + } + } + m_array.sized = model; + m_array.size = glm::vec3{high - low}; + return m_array.size; +} + +glm::dvec3 editor_mode::array_offset(TAnimModel const &Base) +{ + auto const &settings = m_array.settings; + glm::dvec3 offset{0.0}; + if (settings.relative) + offset += glm::dvec3{settings.relative_offset * array_size(Base) * Base.Scale()}; + if (settings.constant) + offset += glm::dvec3{settings.constant_offset}; + return offset; +} + +int editor_mode::array_count(double const Step) const +{ + auto const &settings = m_array.settings; + if (settings.fit == 0) + return std::clamp(settings.count, 1, array_tool::limit); + if (Step < array_tool::minimal_step) + return 1; + // the model, and a copy for each whole offset the length takes + return static_cast(std::min(array_tool::limit, std::floor(std::max(0.0f, settings.length) / Step + 1e-6) + 1.0)); +} + +std::vector> editor_mode::array_placements(TAnimModel &Base) +{ + // a model this large counts as ground itself, and would be put on top of its own copies + float constexpr terrain_model_radius = 50.0f; + + std::vector> places; + auto const &settings = m_array.settings; + auto const offset = array_offset(Base); + auto const count = array_count(glm::length(offset)); + if (count < 2 || glm::length(offset) < array_tool::minimal_step) + return places; + + // the offset goes along the axes of the model, which is turned the way the renderer does it: around y, then x, then z + auto angles = Base.Angles(); + glm::dmat4 turned{1.0}; + turned = glm::rotate(turned, glm::radians(static_cast(angles.y)), glm::dvec3{0.0, 1.0, 0.0}); + turned = glm::rotate(turned, glm::radians(static_cast(angles.x)), glm::dvec3{1.0, 0.0, 0.0}); + turned = glm::rotate(turned, glm::radians(static_cast(angles.z)), glm::dvec3{0.0, 0.0, 1.0}); + auto step = glm::dvec3{turned * glm::dvec4{offset, 0.0}}; + auto location = Base.location(); + places.reserve(count - 1); + for (int idx = 1; idx < count; ++idx) + { + // each copy is turned against the one before it, and leads to the next one the way it faces + location += step; + if (false == simulation::Region->point_inside(location)) + break; // the row ends at the edge of the region, nodes past it are left out of the scene + if (settings.turn != 0.0f) + { + step = glm::rotateY(step, glm::radians(static_cast(settings.turn))); + angles.y = clamp_circular(angles.y + settings.turn, 360.0f); + } + places.emplace_back(location, angles); + } + + if (settings.ground && Base.radius() < terrain_model_radius) + { + // the ground the road tools use. a copy with nothing under it stays at the height the offset gave it + std::vector points; + points.reserve(places.size() + 1); + points.push_back(Base.location()); + for (auto const &place : places) + points.push_back(place.first); + std::vector found; + auto const heights = ground_heights(points, true, &found); + if (found[0]) + { + auto const clearance = points[0].y - heights[0]; + for (std::size_t idx = 0; idx < places.size(); ++idx) + { + if (found[idx + 1]) + places[idx].first.y = heights[idx + 1] + clearance; + } + } + } + return places; +} + +bool editor_mode::array_live() const +{ + // the model the array was made of is still selected, and the array is the last thing done + return m_array.base != nullptr && m_array.base == m_node && false == m_history.empty() && m_history.size() == m_array.step && + m_history.back().action == EditorSnapshot::Action::Array; +} + +void editor_mode::make_array() +{ + auto *base = dynamic_cast(m_node); + if (base == nullptr || false == scene::Layers.editable(base)) + return; + auto const step = glm::length(array_offset(*base)); + if (step < array_tool::minimal_step) + { + m_array.status = "The offsets come to nothing, the copies would sit in the model"; + return; + } + if (array_count(step) < 2) + { + m_array.status = (m_array.settings.fit == 0 ? "An array of one is the model itself, nothing to make" : "The length takes no copy, nothing to make"); + return; + } + + // a single undo step for the whole array, whatever is done to it while it follows the settings + trim_history(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Array; + snap.node_name = base->name(); + snap.position = base->location(); + snap.layer = base->layer(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + m_array.base = base; + m_array.step = m_history.size(); + shape_array(); +} + +void editor_mode::shape_array() +{ + auto *base = m_array.base; + auto &copies = m_history.back().copies; + auto const places = array_placements(*base); + + // the copies there's no place for anymore go + while (copies.size() > places.size()) + { + auto *model = copies.back().model; + copies.pop_back(); + if (model == nullptr) + continue; + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + } + + std::string definition; + for (std::size_t idx = 0; idx < places.size(); ++idx) + { + TAnimModel *model = nullptr; + if (idx < copies.size()) + { + model = copies[idx].model; + m_editor.translate(model, places[idx].first, true); // true: the height is set as well + } + else + { + if (definition.empty()) + base->export_as_text(definition); + // NOTE: no names for the copies, same as with the brush + model = simulation::State.create_model(definition, std::string{}, places[idx].first); + if (model == nullptr) + break; + // the copies go where the model is, rather than to the active layer + scene::Layers.move(model, base->layer()); + EditorSnapshot::array_copy copy; + copy.model = model; + copy.uuid = model->uuid; + copies.push_back(std::move(copy)); + } + model->Angles(places[idx].second); + model->Scale(base->Scale()); + } + + m_array.applied = m_array.settings; + m_array.location = base->location(); + m_array.angles = base->Angles(); + m_array.scale = base->Scale(); + if (copies.size() < places.size()) + m_array.status = "Only " + std::to_string(copies.size()) + " of " + std::to_string(places.size()) + " copies could be made"; + else if (copies.empty()) + m_array.status = "No copies with these settings"; + else + m_array.status = "The model and " + std::to_string(copies.size()) + (copies.size() == 1 ? " copy" : " copies") + "; Ctrl+Z takes the array back"; +} + +void editor_mode::update_array() +{ + if (m_array.base == nullptr) + return; + if (false == array_live()) + { + // once left, the copies are models like any other + m_array.base = nullptr; + return; + } + auto const *base = m_array.base; + if (m_array.settings == m_array.applied && base->location() == m_array.location && base->Angles() == m_array.angles && base->Scale() == m_array.scale) + return; + shape_array(); +} + +void editor_mode::restore_array_snapshot(EditorSnapshot Snapshot, std::vector &Opposite, bool const Undo) +{ + // either way the array is done with following the settings + m_array.base = nullptr; + for (auto © : Snapshot.copies) + { + if (Undo) + { + // NOTE: a copy which was deleted and brought back is another model by now, found by its uuid + auto *model = dynamic_cast(find_node_by_any(copy.model, copy.uuid.to_string(), std::string{})); + copy.model = nullptr; + copy.serialized.clear(); + if (model == nullptr) + continue; // deleted since, and stays that way + // the copy is brought back the way it is now, with whatever was done to it since the array was made + model->export_as_text(copy.serialized); + copy.name = model->name(); + copy.position = model->location(); + copy.rotation = model->Angles(); + copy.scale = model->Scale(); + if (m_node == model) + { + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + } + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + } + else + { + if (copy.serialized.empty()) + continue; + auto *model = simulation::State.create_model(copy.serialized, copy.name, copy.position); + if (model == nullptr) + continue; + scene::Layers.move(model, Snapshot.layer); + model->Angles(copy.rotation); + model->Scale(copy.scale); + model->uuid = copy.uuid; + add_to_hierarchy(model); + copy.model = model; + } + } + Opposite.push_back(std::move(Snapshot)); +} + +void editor_mode::render_array() +{ + auto &settings = m_array.settings; + auto *selected = dynamic_cast(m_node); + if (selected != nullptr && false == scene::Layers.editable(selected)) + selected = nullptr; + + ImGui::PushID("array"); + ImGui::TextDisabled("Copies of the selected model, set out in a row"); + ImGui::RadioButton("Fixed count", &settings.fit, 0); + ImGui::SameLine(); + ImGui::RadioButton("Fit length", &settings.fit, 1); + ImGui::SetNextItemWidth(120.0f); + if (settings.fit == 0) + { + ImGui::InputInt("Count", &settings.count); + settings.count = std::clamp(settings.count, 1, array_tool::limit); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Number of the models in the array, the selected one included"); + } + else + { + ImGui::InputFloat("Length (m)", &settings.length, 1.0f, 10.0f, "%.2f"); + settings.length = std::max(0.0f, settings.length); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "As many copies are made as fit in this length, measured along the row"); + } + + ImGui::Checkbox("Relative offset", &settings.relative); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n" + "1 along an axis puts the copies end to end"); + if (settings.relative) + { + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloat3("x size", &settings.relative_offset.x, 0.01f, 0.0f, 0.0f, "%.3f"); + } + ImGui::Checkbox("Constant offset", &settings.constant); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Distance between the copies in metres, along the axes of the model: x, y (up), z.\n" + "With both offsets on the copies are set apart by their sum"); + if (settings.constant) + { + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloat3("m", &settings.constant_offset.x, 0.05f, 0.0f, 0.0f, "%.3f"); + } + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat("Turn per copy (deg)", &settings.turn, 0.1f, -180.0f, 180.0f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Each copy is turned by this much around the vertical axis against the one before it,\n" + "and the row bends with them into an arc"); + ImGui::Checkbox("Follow the ground", &settings.ground); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The copies are put as high over the ground as the model is, instead of level with it.\n" + "Not for the models large enough to count as ground themselves"); + + if (selected != nullptr) + { + auto const size = array_size(*selected) * selected->Scale(); + auto const step = glm::length(array_offset(*selected)); + ImGui::TextDisabled("Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart", size.x, size.y, size.z, array_count(step), step); + } + + if (array_live()) + { + // NOTE: kept away from the place of the other button, a click too many on which would make the array twice + ImGui::TextDisabled("The array follows the settings"); + ImGui::SameLine(); + if (ImGui::Button("Done")) + m_array.base = nullptr; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The copies stay the way they are. Selecting or changing anything else does the same"); + } + else if (selected != nullptr) + { + if (ImGui::Button("Make array")) + make_array(); + } + else + { + ImGui::TextDisabled("Select a model to make an array of"); + } + if (false == m_array.status.empty()) + ImGui::TextUnformatted(m_array.status.c_str()); + ImGui::PopID(); +} + void editor_mode::render_gizmo_options() { if (ImGui::Button(Global.EditorOrtho ? "3D view (O)" : "Top view, orthographic (O)")) @@ -3760,7 +4139,8 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::Rotate ? "ROT" : s.action == EditorSnapshot::Action::Scale ? "SCA" : s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : - s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : "OTH", + s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : + s.action == EditorSnapshot::Action::Array ? "ARR" : "OTH", s.node_name.empty() ? "(noname)" : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); diff --git a/application/editormode.h b/application/editormode.h index fb44b830..9c5bc317 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -107,7 +107,19 @@ class editor_mode : public application_mode, private editor_track::observer struct stored_profile; struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array }; + // copy of a model an array made + struct array_copy + { + TAnimModel *model{nullptr}; // nullptr while the copy is out of the scene + UID uuid; + // what it takes to make the copy again, filled in when it's taken out of the scene + std::string serialized; + std::string name; + glm::dvec3 position{0.0}; + glm::vec3 rotation{0.0f}; + glm::vec3 scale{1.0f}; + }; Action action{Action::Other}; std::string node_name; // node identifier (basic_node::name()) @@ -127,6 +139,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector infra; // objects which followed the paths bool profiles{false}; // the stored grade line designs changed too, these were the ones before std::vector profile_library; + std::vector copies; // models an array added to the one it was made of }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -307,6 +320,55 @@ class editor_mode : public application_mode, private editor_track::observer bool m_fill_models_as_ground{true}; // large model instances (terrain tiles) count as ground std::string m_fill_status; // result of the last fill, shown in the panel + // array: copies of the selected model set out in a row, after the array modifier of Blender + struct array_settings + { + int fit{0}; // 0: fixed count, 1: as many as the length takes + int count{2}; // the model itself included + float length{10.0f}; // m + bool relative{true}; + glm::vec3 relative_offset{1.0f, 0.0f, 0.0f}; // of the size of the model, along its axes + bool constant{false}; + glm::vec3 constant_offset{1.0f, 0.0f, 0.0f}; // m, along the axes of the model + float turn{0.0f}; // degrees around the vertical axis, each copy against the one before it + bool ground{false}; // the copies are put as high over the ground as the model is + bool operator==(array_settings const &) const = default; + }; + struct array_tool + { + static constexpr int limit = 1000; // models in an array, so a slip of the finger doesn't freeze the editor + static constexpr double minimal_step = 0.001; // m, copies any closer would sit in one another + array_settings settings; + // the array made last, which follows the settings until something else is selected or changed + TAnimModel *base{nullptr}; // model it was made of, nullptr if there's none + std::size_t step{0}; // size of the undo history with the array at its top + array_settings applied; // what the copies were set out with + glm::dvec3 location{0.0}; // of the model then + glm::vec3 angles{0.0f}; + glm::vec3 scale{1.0f}; + // size of the model asked about last + TModel3d const *sized{nullptr}; + glm::vec3 size{0.0f}; + std::string status; // outcome of the last operation, shown in the panel + }; + array_tool m_array; + void render_array(); + // size of the model of specified instance along its axes, the scale of the instance left out + glm::vec3 array_size(TAnimModel const &Base); + // offset of each copy from the one before it, along the axes of the model + glm::dvec3 array_offset(TAnimModel const &Base); + // number of the models in the array, the one it's made of included. Step: length of the offset + int array_count(double const Step) const; + // location and angles of each copy in turn + std::vector> array_placements(TAnimModel &Base); + // true while the array made last follows the settings + bool array_live() const; + void make_array(); + // brings the copies of the array made last to what the settings ask for + void shape_array(); + void update_array(); + void restore_array_snapshot(EditorSnapshot Snapshot, std::vector &Opposite, bool const Undo); + // ImGuizmo-based transform gizmo for the selected node enum class gizmo_operation { translate, rotate, scale }; void render_gizmo(); diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 8015499f..3fee3e3e 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -103,6 +103,11 @@ void editor_ui::render_header_sections() m_itempropertiespanel.render_body(); ImGui::Unindent(); } + if (ImGui::CollapsingHeader("Array")) + { + if (m_arrayoptions) + m_arrayoptions(); + } } void editor_ui::render_menu_contents() diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f4e9679a..b38c2685 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -66,6 +66,11 @@ class editor_ui : public ui_layer { m_gizmooptions = std::move(Renderer); } + // draws the array settings in the toolset window (the array state lives in the editor mode) + void set_array_options(std::function Renderer) + { + m_arrayoptions = std::move(Renderer); + } // actions of the file menu, performed by the editor mode void set_file_actions(std::function Save, std::function Export); // shows outcome of an operation in the layers window @@ -90,6 +95,7 @@ class editor_ui : public ui_layer std::function m_filloptions; std::function m_menuoptions; std::function m_gizmooptions; + std::function m_arrayoptions; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any bool m_track{false}; };