diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f52e785..818849fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,7 @@ set(SOURCES "application/editormodeparallel.cpp" "application/editormodelineside.cpp" "application/editormodevehicle.cpp" +"application/editormodeselection.cpp" "application/editormodesignal.cpp" "application/editormodesweep.cpp" "application/editormodeturntable.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 05e1c736..0bec52a7 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include "application/editormode.h" +#include "scene/scenenodegroups.h" #include "application/editoruilayer.h" #ifndef _WIN32 #include @@ -512,11 +513,13 @@ editor_mode::editor_mode() { ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); ui()->set_open_scenery([this]() { m_openscenery_asked = true; }); + ui()->set_edit_menu([this]() { render_edit_menu(); }); ui()->set_menu_options([this]() { render_object_menu(); render_track_menu(); render_road_menu(); render_map_menu(); + render_help_menu(); }); m_roadtool.settings.normalize(); road_select(nullptr); @@ -672,7 +675,7 @@ void editor_mode::start_focus(scene::basic_node *node, double duration) m_focus_duration = duration; } -void editor_mode::snap_to_ground(scene::basic_node *node) +void editor_mode::snap_to_ground(scene::basic_node *node, bool const Alone, bool const Joined) { if (!node || !simulation::Region) return; @@ -762,8 +765,19 @@ void editor_mode::snap_to_ground(scene::basic_node *node) return; push_snapshot(node, EditorSnapshot::Action::Move); + if (false == m_history.empty()) + m_history.back().joined = Joined; glm::dvec3 target = origin; target.y = bestY; + if (auto *model = dynamic_cast(node); Alone && model != nullptr && node->group() > 1) + { + // the model of a group goes down by itself, the rest of the group stays + simulation::Region->erase(model); + model->location(target); + simulation::Region->insert(model); + model->mark_dirty(); + return; + } m_editor.translate(node, target, true); // true == apply the computed Y (free vertical move) } @@ -924,6 +938,9 @@ 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()); + // and out of the selection and their group + m_selection.erase(std::remove(m_selection.begin(), m_selection.end(), node), m_selection.end()); + scene::Groups.remove(node); // an array no longer follows its settings once the model it was made of is gone if (m_array.base == node) @@ -931,6 +948,28 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) } void editor_mode::undo_last() +{ + // the changes made together are taken back together, and their models are the selection again + std::vector touched; + while (false == m_history.empty()) + { + auto const joined{m_history.back().joined}; + undo_one(); + if (m_node != nullptr && std::find(touched.begin(), touched.end(), m_node) == touched.end()) + touched.push_back(m_node); + if (false == joined) + break; + } + if (touched.size() > 1) + { + // the one with the gizmo was recorded first, so it's taken back last + m_node = touched.back(); + m_selection.assign(touched.begin(), touched.end() - 1); + ui()->set_node(m_node); + } +} + +void editor_mode::undo_one() { if (m_history.empty()) return; @@ -998,6 +1037,7 @@ void editor_mode::undo_last() redoSnap.node_ptr = nullptr; redoSnap.uuid = snap.uuid; redoSnap.layer = snap.layer; + redoSnap.joined = snap.joined; g_redo.push_back(std::move(redoSnap)); TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position); @@ -1022,6 +1062,7 @@ void editor_mode::undo_last() EditorSnapshot current; current.action = snap.action; + current.joined = snap.joined; current.node_name = snap.node_name; current.node_ptr = target; current.layer = target->layer(); @@ -1065,6 +1106,24 @@ void editor_mode::undo_last() } void editor_mode::redo_last() +{ + // the first of the changes made together is on the top, the others follow it + std::vector touched; + do + { + redo_one(); + if (m_node != nullptr && std::find(touched.begin(), touched.end(), m_node) == touched.end()) + touched.push_back(m_node); + } while (false == g_redo.empty() && g_redo.back().joined); + if (touched.size() > 1) + { + m_node = touched.front(); + m_selection.assign(touched.begin() + 1, touched.end()); + ui()->set_node(m_node); + } +} + +void editor_mode::redo_one() { if (g_redo.empty()) return; @@ -1131,6 +1190,7 @@ void editor_mode::redo_last() hist.position = snap.position; hist.uuid = snap.uuid; hist.layer = snap.layer; + hist.joined = snap.joined; m_history.push_back(std::move(hist)); // NOTE: located the way other steps do it, as the name alone doesn't lead to a node which has none @@ -1153,6 +1213,7 @@ void editor_mode::redo_last() EditorSnapshot hist; hist.action = snap.action; + hist.joined = snap.joined; hist.node_name = snap.node_name; hist.node_ptr = target; @@ -1429,6 +1490,9 @@ bool editor_mode::update() render_new_scenery_popup(); render_open_scenery_popup(); + render_shortcuts_window(); + draw_selection_overlay(); + render_context_menu(); // --- ImGui: Editor Settings & History windows --- if(m_settings_open) @@ -3672,6 +3736,8 @@ void editor_mode::choose_edit_mode(nodebank_panel::edit_mode const Mode) m_track_window_open = false; m_roadtool.window = false; terrain_workspace(false); + if (Mode != nodebank_panel::MODIFY) + m_selection.clear(); ui()->set_mode(Mode); } @@ -3726,6 +3792,7 @@ void editor_mode::render_workspaces() {STR_C("Roads"), work_area::roads, STR_C("Roads and junctions, level crossings, the points where the traffic comes and goes")}, {STR_C("Terrain"), work_area::terrain, STR_C("Terrain patches and chunks, sculpting, streaming, orthophoto")}, }; + char const *const keys[] = {"F2", "F3", "F4", "F5"}; auto const current{current_work_area()}; auto const &style{ImGui::GetStyle()}; auto *list{ImGui::GetWindowDrawList()}; @@ -3757,7 +3824,7 @@ void editor_mode::render_workspaces() if (ui_layer::font_bold == nullptr) list->AddText(font, fontsize, ImVec2(textposition.x + 1.0f, textposition.y), color, entry.label); if (hovered) - ImGui::SetTooltip("%s", entry.tooltip); + ImGui::SetTooltip("%s [%s]", entry.tooltip, keys[static_cast(entry.which)]); ImGui::SameLine(0.0f, style.ItemSpacing.x); } } @@ -3833,13 +3900,170 @@ void editor_mode::render_toolbar() { if (ImGui::MenuItem(operation.first, nullptr, m_gizmo_op == operation.second)) m_gizmo_op = operation.second; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values")); } if (m_gizmo_op != gizmo_operation::scale && ImGui::MenuItem(STR_C("Local space (R)"), nullptr, m_gizmo_local)) m_gizmo_local = !m_gizmo_local; + if (m_gizmo_op != gizmo_operation::scale && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The handles along the axes of the selected model instead of the axes of the world")); ImGui::Separator(); } if (ImGui::MenuItem(STR_C("Top view, orthographic (O)"), nullptr, Global.EditorOrtho)) toggle_ortho(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); +} + +void editor_mode::render_edit_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Edit"))) + return; + if (ImGui::MenuItem(STR_C("Undo"), "Ctrl+Z", false, false == m_history.empty())) + undo_last(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Takes the last change back")); + if (ImGui::MenuItem(STR_C("Redo"), "Ctrl+Y", false, false == g_redo.empty())) + redo_last(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Makes the change taken back again")); + ImGui::Separator(); + auto const selected{m_node != nullptr}; + if (ImGui::MenuItem(STR_C("Delete"), STR_C("Del"), false, selected || m_instance != 0)) + delete_selected(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Deletes what's selected: a model, tracks, a road piece or point, a signal, an include of a scenery template")); + auto const models{dynamic_cast(m_node) != nullptr && ui()->mode() != nodebank_panel::TRACK}; + if (ImGui::MenuItem(STR_C("Copy"), "Ctrl+C", false, models)) + copy_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Copies the selected models with their groups")); + if (ImGui::MenuItem(STR_C("Paste"), "Ctrl+V", false, false == m_clipboard.empty())) + paste_clipboard(cursor_ground()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts the copied models at the cursor, around it as they were around the selected one; Ctrl+RMB in the view pastes them where it was clicked")); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Group"), "Ctrl+G", false, models && selected_nodes().size() > 1)) + group_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The selected models move together from now on; a click on one selects them all. Shift+LMB adds a model to the selection")); + if (ImGui::MenuItem(STR_C("Ungroup"), "Ctrl+Shift+G", false, selected && m_node->group() > 1)) + ungroup_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The models of the group move each by itself again")); + if (ImGui::MenuItem(STR_C("Drop to the ground"), STR_C("End"), false, selected)) + drop_selection_to_ground(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it")); + if (ImGui::MenuItem(STR_C("Fly to the selected"), "F", false, selected)) + start_focus(m_node, 0.6); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The camera flies to the selected model and looks at it")); + if (ImGui::MenuItem(STR_C("Bend along the track"), "B", false, ui()->mode() != nodebank_panel::TRACK)) + bend_shortcut(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Bends the selected model, e.g. a platform edge or a fence, along the nearest track; the settings are in the inspector")); + ImGui::EndMenu(); +} + +void editor_mode::render_help_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Help"))) + return; + ImGui::MenuItem(STR_C("Keyboard shortcuts"), "F1", &m_shortcuts_open); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The keys of the editor, for each field of work")); + ImGui::EndMenu(); +} + +void editor_mode::render_shortcuts_window() +{ + if (false == m_shortcuts_open) + return; + struct shortcut + { + char const *keys; + char const *action; + }; + struct group + { + char const *title; + std::vector shortcuts; + }; + static std::vector const groups{ + {"General", + {{"F1", "This list of shortcuts"}, + {"Ctrl+S", "Save the scenery"}, + {"Ctrl+Z / Ctrl+Y", "Undo / redo"}, + {"Ctrl+Space", "Unpin all docks for the most of the view, or pin them back"}, + {"F9", "Log"}, + {"PrtScr", "Screenshot"}, + {"Ctrl+Shift+F11", "Export scenery dump"}, + {"F11", "Leave the editor"}, + {"F10", "Quit"}}}, + {"Fields of work", {{"F2", "Surroundings"}, {"F3", "Tracks"}, {"F4", "Roads"}, {"F5", "Terrain"}}}, + {"Camera", + {{"RMB held, mouse", "Look around"}, + {"RMB held, W S A D", "Fly forwards, back, left, right (arrows in the legacy scheme)"}, + {"RMB held, E / Q", "Fly up / down (Page Up / Page Down in the legacy scheme)"}, + {"F", "Fly to the selected"}, + {"O", "Top view, orthographic"}}}, + {"Gizmo", + {{"Q / W / E", "Translate / rotate / scale"}, {"R", "Local space"}, {"Ctrl held while dragging", "Snap to the step set in the gizmo settings"}}}, + {"Surroundings", + {{"1 2 3 4 5", "Select, insert, brush, area fill, copy to bank"}, + {"LMB", "Select a model, or place one in the insert and brush modes"}, + {"Shift+LMB", "Add a model to the selection, or take it out"}, + {"Ctrl+RMB", "Menu of the selection at the cursor"}, + {"Ctrl+C / Ctrl+V", "Copy the selected models / paste them at the cursor"}, + {"Ctrl+G / Ctrl+Shift+G", "Group the selected models / take their group apart"}, + {"End", "Drop the selected onto the ground, each model of a group by itself"}, + {"Del", "Delete the selected"}, + {"B", "Bend the selected along the track"}, + {"Backspace", "Area fill: take the last point of the outline back"}}}, + {"Tracks", + {{"Esc", "Select tool; cancels what's going on first"}, + {"L / T / U", "Lay track / switch / turntable"}, + {"G / C", "Straight / curve"}, + {"H / B", "Signals / objects along the track"}, + {"P / V / J / I", "Profile / speed / joints / infrastructure"}, + {"Ctrl+F", "Find a track by its name"}, + {"Shift+LMB", "Add a track to the selection, drag: a box"}, + {"RMB on a track", "What can be done with it"}, + {"K", "Split the selected track at the cursor"}, + {"Enter / Backspace", "Laying track: finish / take the last point back"}, + {"Del", "Delete the selected tracks"}}}, + {"Roads", + {{"Esc", "Finish the road being built"}, + {"Ctrl held", "Build: straight ahead"}, + {"Shift+LMB", "Select: add a point, or take it out"}, + {"K", "Split the road at the cursor, or make a point there"}, + {"Del", "Delete the selected"}}}, + {"Terrain", {{"LMB", "Sculpt: raise; smooth: even out; chunks: add one"}, {"Shift+LMB", "Sculpt: lower; chunks: delete one"}}}, + }; + ImGui::SetNextWindowSize(ImVec2(620.0f * Global.ui_scale, 560.0f * Global.ui_scale), ImGuiCond_FirstUseEver); + // it floats over the view, read better on a solid background + ImGui::SetNextWindowBgAlpha(0.97f); + if (ImGui::Begin((std::string(STR_C("Keyboard shortcuts")) + "###shortcuts").c_str(), &m_shortcuts_open, ImGuiWindowFlags_NoCollapse)) + { + for (auto const &entry : groups) + { + ImGui::SeparatorText(STR_C(entry.title)); + if (false == ImGui::BeginTable(entry.title, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + continue; + ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 190.0f * Global.ui_scale); + ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch); + for (auto const &item : entry.shortcuts) + { + ImGui::TableNextColumn(); + ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_CheckMark), "%s", STR_C(item.keys)); + ImGui::TableNextColumn(); + ImGui::TextWrapped("%s", STR_C(item.action)); + } + ImGui::EndTable(); + } + } + ImGui::End(); } void editor_mode::render_gizmo_options() @@ -4004,7 +4228,8 @@ void editor_mode::render_gizmo() // record a single undo snapshot at the start of the drag if (!m_gizmo_using) { - push_snapshot(m_node, action); + // the models selected along with it and their groups go into the same step of the history + selection_snapshots(action); m_gizmo_using = true; } @@ -4015,7 +4240,9 @@ void editor_mode::render_gizmo() { // apply the rotation delta relative to the model's current orientation glm::vec3 const newangles(newrotation[0], newrotation[1], newrotation[2]); - m_editor.rotate(model, newangles - model->Angles(), 0.0f); + auto const turned{newangles - model->Angles()}; + m_editor.rotate(model, turned, 0.0f); + selection_follow(glm::dvec3(0.0), turned, model->location()); } else if (operation == ImGuizmo::SCALE && model) { @@ -4025,7 +4252,9 @@ void editor_mode::render_gizmo() { glm::dvec3 const newworldpos = camerapos + glm::dvec3(newtranslation[0], newtranslation[1], newtranslation[2]); // pass Snaptoground == true so the gizmo's Y component is applied (free 3D move) + auto const before{m_node->location()}; m_editor.translate(m_node, newworldpos, true); + selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location()); } } else @@ -4331,6 +4560,76 @@ void editor_mode::exit() } } +void editor_mode::delete_model(TAnimModel *model, bool const Joined) +{ + // record deletion for undo (serialize full node) + std::string as_text; + + model->export_as_text(as_text); + std::string debug = "Deleting node: " + as_text + "\nSerialized data:\n"; + push_snapshot(model, EditorSnapshot::Action::Delete, as_text); + if (false == m_history.empty()) + m_history.back().joined = Joined; + WriteLog(debug, logtype::generic); + + // clear history pointers referencing this model before actually deleting it + nullify_history_pointers(model); + remove_from_hierarchy(model); + + if (m_node == model) + { + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + } + simulation::State.delete_model(model); +} + +void editor_mode::delete_selected() +{ + // what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks + if (road_delete()) + return; + if (m_instance != 0 && signal_delete(m_instance)) + return; + if (m_instance != 0) + { + // include of a scenery template. what it shows goes out of sight, the directive is erased on save + scene::Layers.removed(m_instance, true); + EditorSnapshot snap; + snap.instance = m_instance; + snap.node_name = *scene::Layers.instance(m_instance).file; + m_history.push_back(std::move(snap)); + g_redo.clear(); + ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved."); + select_include(0); + return; + } + TAnimModel *model = dynamic_cast(m_node); + if (model) + { + // the models of the selection go together, one step of the history + auto joined{false}; + for (auto *node : selected_nodes()) + { + if (auto *selected = dynamic_cast(node)) + { + delete_model(selected, joined); + joined = true; + } + } + m_selection.clear(); + } + else if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2) + { + track_set_delete(); + } + else if (selected_track() != nullptr) + { + delete_selected_track(); + } +} + void editor_mode::on_key(int const Key, int const Scancode, int const Action, int const Mods) { #ifndef __unix__ @@ -4375,6 +4674,12 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in case GLFW_KEY_E: m_gizmo_op = gizmo_operation::scale; break; case GLFW_KEY_R: m_gizmo_local = !m_gizmo_local; break; case GLFW_KEY_B: handled = ui()->mode() != nodebank_panel::TRACK && bend_shortcut(); break; + case GLFW_KEY_F1: m_shortcuts_open = !m_shortcuts_open; break; + // the fields of work in the order of their row + case GLFW_KEY_F2: choose_work_area(work_area::surroundings); break; + case GLFW_KEY_F3: choose_work_area(work_area::tracks); break; + case GLFW_KEY_F4: choose_work_area(work_area::roads); break; + case GLFW_KEY_F5: choose_work_area(work_area::terrain); break; // the edit modes in the order of the toolbar; in the track mode the digits typed as a value were taken already case GLFW_KEY_1: choose_edit_mode(nodebank_panel::MODIFY); break; case GLFW_KEY_2: choose_edit_mode(nodebank_panel::ADD); break; @@ -4395,6 +4700,23 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in return; } + // the selection: copy, paste at the cursor, group and ungroup. ahead of the camera keys, the letters may be bound there + if (Global.ctrlState && (Key == GLFW_KEY_C || Key == GLFW_KEY_V || Key == GLFW_KEY_G) && ui()->mode() != nodebank_panel::TRACK && false == m_roadtool.window) + { + if (is_press(Action)) + { + if (Key == GLFW_KEY_C) + copy_selection(); + else if (Key == GLFW_KEY_V) + paste_clipboard(cursor_ground()); + else if (Global.shiftState) + ungroup_selection(); + else + group_selection(); + } + return; + } + // save; checked ahead of the camera keys, which have S bound to moving back if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S) { @@ -4459,58 +4781,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_DELETE: - if (is_press(Action) && road_delete()) - { - break; - } - if (is_press(Action) && m_instance != 0 && signal_delete(m_instance)) - { - break; - } - if (is_press(Action) && m_instance != 0) - { - // include of a scenery template. what it shows goes out of sight, the directive is erased on save - scene::Layers.removed(m_instance, true); - EditorSnapshot snap; - snap.instance = m_instance; - snap.node_name = *scene::Layers.instance(m_instance).file; - m_history.push_back(std::move(snap)); - g_redo.clear(); - ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved."); - select_include(0); - break; - } if (is_press(Action)) - { - TAnimModel *model = dynamic_cast(m_node); - if (model) - { - // record deletion for undo (serialize full node) - std::string as_text; - - model->export_as_text(as_text); - std::string debug = "Deleting node: " + as_text + "\nSerialized data:\n"; - push_snapshot(model, EditorSnapshot::Action::Delete, as_text); - WriteLog(debug, logtype::generic); - - // clear history pointers referencing this model before actually deleting it - nullify_history_pointers(model); - remove_from_hierarchy(model); - - m_node = nullptr; - m_dragging = false; - ui()->set_node(nullptr); - simulation::State.delete_model(model); - } - else if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2) - { - track_set_delete(); - } - else if (selected_track() != nullptr) - { - delete_selected_track(); - } - } + delete_selected(); break; case GLFW_KEY_ESCAPE: @@ -4569,11 +4841,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_F: - if (is_press(Action)) + if (is_press(Action) && m_node) { - if(!m_node) - break; - // start smooth focus camera on selected node start_focus(m_node, 0.6); } @@ -4585,7 +4854,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // Unreal-style "snap to floor": drop the selected node onto the surface below it. // works against triangle geometry (shape_node terrain / opaque shapes); once a proper // editable terrain mesh exists, dropping onto it works without further changes here. - snap_to_ground(m_node); + drop_selection_to_ground(); } break; @@ -4689,12 +4958,15 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const // delegate node picking behaviour depending on current panel mode. // NOTE: the pick is resolved a few frames later, often after the button was already released, // so the callback mustn't depend on the button still being held + // Shift+LMB adds a model to the selection, or takes it out + auto const additive{mode == nodebank_panel::MODIFY && (Mods & GLFW_MOD_SHIFT) != 0}; GfxRenderer->Pick_Node_Callback( - [this, mode, rotation_mode, fixed_rotation_value](scene::basic_node *node) { + [this, mode, rotation_mode, fixed_rotation_value, additive](scene::basic_node *node) { // the press turned out to be meant for the UI if (!viewport_click()) return; + auto *const previous{m_node}; m_node = nullptr; select_include(0); @@ -4721,12 +4993,17 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (node) { double const dist = glm::distance(node->location(), glm::dvec3{Global.pCamera.Pos}); if (dist > static_cast(kMaxPlacementDistance)) + { + if (additive) + m_node = previous; return; + } } if (mode == nodebank_panel::MODIFY) { - m_node = node; - ui()->set_node(m_node); + if (additive) + m_node = previous; + select_node(node, additive); } else if (mode == nodebank_panel::COPY) { @@ -4801,6 +5078,23 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const } else if (Button == GLFW_MOUSE_BUTTON_RIGHT) { + // Ctrl+RMB: the menu at the cursor, for the model under it or the selection; RMB alone turns the camera + if ((Mods & GLFW_MOD_CONTROL) != 0 && ui()->mode() != nodebank_panel::TRACK) + { + if (is_press(Action)) + { + m_contextpoint = cursor_ground(); + GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { + if (!viewport_click()) + return; + auto const selection{selected_nodes()}; + if (node != nullptr && false == node->from_template() && scene::Layers.editable(node) && std::find(selection.begin(), selection.end(), node) == selection.end()) + select_node(node, false); + m_contextopen = true; + }); + } + return; + } if (ui()->mode() == nodebank_panel::TRACK) { if (is_press(Action)) diff --git a/application/editormode.h b/application/editormode.h index 4251a36c..45847a91 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -96,6 +96,7 @@ class editor_mode : public application_mode, private editor_track::observer } void on_event_poll() override; bool is_command_processor() const override; + // takes the last change back; changes made together (e.g. to the models of a selection) are taken back together void undo_last(); static bool focus_active(); static void set_focus_active(bool isActive); @@ -166,6 +167,7 @@ class editor_mode : public application_mode, private editor_track::observer 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 + bool joined{false}; // undone and redone together with the change before it }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -176,6 +178,9 @@ class editor_mode : public application_mode, private editor_track::observer editor_ui *ui() const; void redo_last(); + // a single step of the history, see undo_last() + void undo_one(); + void redo_one(); void handle_brush_mouse_hold(int Action, int Button); void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); // members @@ -222,12 +227,54 @@ class editor_mode : public application_mode, private editor_track::observer // puts every instance of the model the selected node shows on the ground under it void drop_model_instances(); void render_object_menu(); + // undo, redo and what can be done with the selection, with their keys + void render_edit_menu(); + // the list of the keyboard shortcuts, opened from the help menu or by F1 + void render_help_menu(); + void render_shortcuts_window(); + bool m_shortcuts_open{false}; + // deletes what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks (Del) + void delete_selected(); + void delete_model(TAnimModel *Model, bool const Joined); + + // more models selected at once, in the surroundings: m_node is the one the gizmo is on, the others are in m_selection. + // Shift+LMB adds a model or takes it out; a model of a group stands for the whole group + std::vector m_selection; + std::vector selected_nodes() const; // m_node first + std::vector selection_with_groups() const; // and the other models of their groups + void select_node(scene::basic_node *Node, bool const Additive); + void draw_selection_overlay(); + // the other models of the selection follow m_node moved or turned by the gizmo + void selection_snapshots(EditorSnapshot::Action const Action); + void selection_follow(glm::dvec3 const &Moved, glm::vec3 const &Turned, glm::dvec3 const &Pivot); + // clipboard (Ctrl+C, Ctrl+V): definitions of the models, placed around the cursor as they were around the selected one + struct clipboard_entry + { + std::string definition; + glm::dvec3 offset{0.0}; + glm::vec3 angles{0.0f}; + glm::vec3 scale{1.0f}; + int group{-1}; // models copied with their group are grouped again, each group anew + }; + std::vector m_clipboard; + void copy_selection(); + void paste_clipboard(glm::dvec3 const &Location); + glm::dvec3 cursor_ground(); // the point of the ground under the cursor + // groups (Ctrl+G, Ctrl+Shift+G), written to the scenery as group ... endgroup + void group_selection(); + void ungroup_selection(); + void drop_selection_to_ground(); + // menu at the cursor, Ctrl+RMB + bool m_contextopen{false}; + glm::dvec3 m_contextpoint{0.0}; + void render_context_menu(); // focus camera smoothly on specified node void start_focus(scene::basic_node *node, double duration = 0.6); - // drops the node straight down onto the nearest surface below (terrain or another object) - void snap_to_ground(scene::basic_node *node); + // drops the node straight down onto the nearest surface below (terrain or another object). its group goes along with it, + // unless Alone: then the node goes by itself. Joined: undone together with the change before + void snap_to_ground(scene::basic_node *node, bool const Alone = false, bool const Joined = false); // editable terrain patches created in the editor void render_terrain_ui(); diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp index 5e0f3bff..0ff5ef67 100644 --- a/application/editormoderoad.cpp +++ b/application/editormoderoad.cpp @@ -220,12 +220,16 @@ void editor_mode::render_road_menu() { if (ImGui::BeginMenu("Roads")) { - if (ImGui::MenuItem("Road editor", nullptr, &m_roadtool.window) && m_roadtool.window) + if (ImGui::MenuItem("Road editor", "F4", &m_roadtool.window) && m_roadtool.window) { m_track_window_open = false; terrain_workspace(false); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The roads as the field of work: their tools in the toolbar, their settings in the tool options"); ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Draws the lanes of the roads with the ways the vehicles take through the junctions"); ImGui::EndMenu(); } } diff --git a/application/editormodeselection.cpp b/application/editormodeselection.cpp new file mode 100644 index 00000000..a8c84533 --- /dev/null +++ b/application/editormodeselection.cpp @@ -0,0 +1,362 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// more models selected at once, the clipboard, groups and the menu at the cursor of the scenery editor + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" + +#include "model/AnimModel.h" +#include "rendering/renderer.h" +#include "scene/scenelayers.h" +#include "scene/scenenodegroups.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/translation.h" +#include "imgui/imgui.h" + +#include +#include +#include + +namespace +{ + +// the models of the selection can be grouped, copied and dropped onto the ground; tracks and other nodes are left out +TAnimModel *as_model(scene::basic_node *Node) +{ + return Node != nullptr ? dynamic_cast(Node) : nullptr; +} + +} // namespace + +std::vector editor_mode::selected_nodes() const +{ + std::vector nodes; + if (m_node == nullptr) + return nodes; + nodes.push_back(m_node); + // the selection of more models is a thing of the surroundings + if (ui()->mode() != nodebank_panel::MODIFY) + return nodes; + for (auto *node : m_selection) + if (node != nullptr && std::find(nodes.begin(), nodes.end(), node) == nodes.end()) + nodes.push_back(node); + return nodes; +} + +std::vector editor_mode::selection_with_groups() const +{ + auto nodes{selected_nodes()}; + auto const count{nodes.size()}; + for (std::size_t index = 0; index < count; ++index) + { + // NOTE: the groups up to the first one stand for no group, see scene::basic_editor + if (nodes[index]->group() <= 1) + continue; + for (auto *member : scene::Groups.group(nodes[index]->group()).nodes) + if (std::find(nodes.begin(), nodes.end(), member) == nodes.end()) + nodes.push_back(member); + } + return nodes; +} + +void editor_mode::select_node(scene::basic_node *Node, bool const Additive) +{ + if (false == Additive) + { + m_selection.clear(); + m_node = Node; + ui()->set_node(m_node); + return; + } + if (Node == nullptr) + return; + if (m_node == nullptr) + { + m_node = Node; + } + else if (Node == m_node) + { + // taken out; the one selected before it takes the gizmo + if (m_selection.empty()) + { + m_node = nullptr; + } + else + { + m_node = m_selection.back(); + m_selection.pop_back(); + } + } + else if (auto const found{std::find(m_selection.begin(), m_selection.end(), Node)}; found != m_selection.end()) + { + m_selection.erase(found); + } + else + { + // the model selected last gets the gizmo + m_selection.push_back(m_node); + m_node = Node; + } + ui()->set_node(m_node); + auto const count{selected_nodes().size()}; + if (count > 1) + ui()->set_status(format(STR_C("%zu models selected; Shift+LMB adds or takes one out"), count)); +} + +void editor_mode::draw_selection_overlay() +{ + if (ui()->mode() != nodebank_panel::MODIFY || m_node == nullptr) + return; + auto const nodes{selection_with_groups()}; + if (nodes.size() < 2) + return; + // a ring around each model of the selection, the one with the gizmo brighter, the models of their groups dimmer + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const direct{selected_nodes()}; + for (auto *node : nodes) + { + ImVec2 point; + if (false == projection.project(node->location(), point)) + continue; + auto const chosen{std::find(direct.begin(), direct.end(), node) != direct.end()}; + auto const color{node == m_node ? overlay_color::highlight : chosen ? overlay_color::selected : overlay_color::marked}; + drawlist->AddCircle(point, node == m_node ? 9.0f : 7.0f, color, 16, 2.0f); + } +} + +void editor_mode::selection_snapshots(EditorSnapshot::Action const Action) +{ + // one step of the history for all of them + auto joined{false}; + for (auto *node : selection_with_groups()) + { + push_snapshot(node, Action); + if (false == m_history.empty()) + m_history.back().joined = joined; + joined = true; + } +} + +void editor_mode::selection_follow(glm::dvec3 const &Moved, glm::vec3 const &Turned, glm::dvec3 const &Pivot) +{ + // a group is moved by any of its models, once; the one of m_node is moved with it already + std::vector moved; + if (m_node != nullptr && m_node->group() > 1) + moved.push_back(m_node->group()); + for (auto *node : selected_nodes()) + { + if (node == m_node) + continue; + if (node->group() > 1) + { + if (std::find(moved.begin(), moved.end(), node->group()) != moved.end()) + continue; + moved.push_back(node->group()); + } + if (Turned != glm::vec3(0.0f)) + { + // turned in place, then carried around the turning point + m_editor.rotate(node, Turned, 0.0f); + auto const location{Pivot + glm::rotateY(node->location() - Pivot, glm::radians(Turned.y))}; + m_editor.translate(node, location, true); + } + if (Moved != glm::dvec3(0.0)) + m_editor.translate(node, node->location() + Moved, true); + } +} + +glm::dvec3 editor_mode::cursor_ground() +{ + return placement_on_ground(Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position())); +} + +void editor_mode::copy_selection() +{ + auto const nodes{selection_with_groups()}; + auto const *anchor{as_model(m_node)}; + if (anchor == nullptr) + { + ui()->set_status(STR("Select a model to copy"), true); + return; + } + m_clipboard.clear(); + std::map groups; + for (auto *node : nodes) + { + auto *model{as_model(node)}; + if (model == nullptr) + continue; + clipboard_entry entry; + model->export_as_text(entry.definition); + entry.offset = model->location() - anchor->location(); + entry.angles = model->Angles(); + entry.scale = model->Scale(); + if (model->group() > 1) + entry.group = groups.emplace(model->group(), static_cast(groups.size())).first->second; + m_clipboard.push_back(std::move(entry)); + } + ui()->set_status(format(STR_C("%zu models copied; Ctrl+V puts them at the cursor"), m_clipboard.size())); +} + +void editor_mode::paste_clipboard(glm::dvec3 const &Location) +{ + if (m_clipboard.empty()) + { + ui()->set_status(STR("Nothing copied yet; Ctrl+C copies the selected models"), true); + return; + } + // one step of the history, the way an array is: the copies are taken out together + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Array; + std::vector made; + std::map> groups; + for (auto const &entry : m_clipboard) + { + // NOTE: no names for the copies, same as with the brush and the array + auto *model{simulation::State.create_model(entry.definition, std::string{}, Location + entry.offset)}; + if (model == nullptr) + continue; + model->location(Location + entry.offset); + model->Angles(entry.angles); + model->Scale(entry.scale); + add_to_hierarchy(model); + EditorSnapshot::array_copy copy; + copy.model = model; + copy.uuid = model->uuid; + snap.copies.push_back(std::move(copy)); + snap.layer = model->layer(); + made.push_back(model); + if (entry.group >= 0) + groups[entry.group].push_back(model); + } + if (made.empty()) + return; + m_history.push_back(std::move(snap)); + g_redo.clear(); + for (auto const &group : groups) + if (group.second.size() > 1) + scene::Groups.make(group.second); + // the copies are the selection now + m_selection.assign(made.begin() + 1, made.end()); + m_node = made.front(); + ui()->set_node(m_node); + ui()->set_status(format(STR_C("%zu models pasted; Ctrl+Z takes them back"), made.size())); +} + +void editor_mode::group_selection() +{ + auto nodes{selection_with_groups()}; + nodes.erase(std::remove_if(nodes.begin(), nodes.end(), [](scene::basic_node *Node) { return as_model(Node) == nullptr; }), nodes.end()); + if (nodes.size() < 2) + { + ui()->set_status(STR("Select two models or more to group them: Shift+LMB adds one to the selection"), true); + return; + } + for (auto *node : nodes) + { + std::string reason; + if (node->from_template() || false == scene::Layers.editable(node, &reason)) + { + ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be grouped: " + reason, true); + return; + } + } + // a group is written to the scenery as one block, in the layer of the model with the gizmo + for (auto *node : nodes) + if (node->layer() != m_node->layer()) + scene::Layers.move(node, m_node->layer()); + scene::Groups.make(nodes); + m_selection.clear(); + ui()->set_status(format(STR_C("%zu models grouped: a click on one selects them all; Ctrl+Shift+G takes the group apart"), nodes.size())); +} + +void editor_mode::ungroup_selection() +{ + std::vector groups; + for (auto *node : selected_nodes()) + { + if (node->group() <= 1 || std::find(groups.begin(), groups.end(), node->group()) != groups.end()) + continue; + // the groups of the scenery templates are their includes, they can't be taken apart here + auto const &members{scene::Groups.group(node->group()).nodes}; + if (std::any_of(members.begin(), members.end(), [](scene::basic_node const *Member) { return Member->from_template(); })) + continue; + groups.push_back(node->group()); + } + if (groups.empty()) + { + ui()->set_status(STR("The selected model isn't in a group"), true); + return; + } + for (auto const group : groups) + scene::Groups.dissolve(group); + ui()->set_status(format(STR_C("%zu groups taken apart"), groups.size())); +} + +void editor_mode::drop_selection_to_ground() +{ + // each model by itself, a group follows the ground under its models + auto joined{false}; + for (auto *node : selection_with_groups()) + { + auto const size{m_history.size()}; + snap_to_ground(node, true, joined); + joined = joined || m_history.size() != size; + } +} + +void editor_mode::render_context_menu() +{ + if (m_contextopen) + { + ImGui::OpenPopup("##editorcontext"); + m_contextopen = false; + } + if (false == ImGui::BeginPopup("##editorcontext")) + return; + auto const selected{m_node != nullptr}; + auto const model{as_model(m_node) != nullptr}; + auto const count{selected_nodes().size()}; + if (count > 1) + ImGui::TextDisabled(STR_C("%zu models selected"), count); + else if (selected) + ImGui::TextDisabled("%s", m_node->name().empty() ? "(unnamed node)" : m_node->name().c_str()); + else + ImGui::TextDisabled("%s", STR_C("Nothing selected")); + ImGui::Separator(); + auto const item = [](char const *Label, char const *Keys, bool const Enabled, char const *Tooltip) { + auto const chosen{ImGui::MenuItem(Label, Keys, false, Enabled)}; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", Tooltip); + return chosen; + }; + if (item(STR_C("Copy"), "Ctrl+C", model, STR_C("Copies the selected models with their groups"))) + copy_selection(); + if (item(STR_C("Paste here"), "Ctrl+V", false == m_clipboard.empty(), STR_C("Puts the copied models here, around this point as they were around the selected one"))) + paste_clipboard(m_contextpoint); + ImGui::Separator(); + if (item(STR_C("Group"), "Ctrl+G", model && count > 1, STR_C("The selected models move together from now on; a click on one selects them all"))) + group_selection(); + if (item(STR_C("Ungroup"), "Ctrl+Shift+G", selected && m_node->group() > 1, STR_C("The models of the group move each by itself again"))) + ungroup_selection(); + if (item(STR_C("Drop to the ground"), STR_C("End"), selected, STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it"))) + drop_selection_to_ground(); + ImGui::Separator(); + if (item(STR_C("Fly to the selected"), "F", selected, STR_C("The camera flies to the selected model and looks at it"))) + start_focus(m_node, 0.6); + if (item(STR_C("Delete"), STR_C("Del"), selected, STR_C("Deletes the selected models"))) + delete_selected(); + ImGui::EndPopup(); +} diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 8bb4f2a5..a2281e6c 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -53,6 +53,11 @@ http://mozilla.org/MPL/2.0/. // middle, point), height (origin, bottom, top, edge, point), point=,,, // pick=,, (a point of the scene picked as the reference point), go (0, 1) // selectinclude the include placed nearest the point +// addmodel the model instance nearest the point added to the selection, or taken out (Shift+LMB) +// selection logs the selected models, with the models of their groups +// moveselection the selection moved by the offset, as the gizmo moves it +// turnselection the selection turned around the model with the gizmo +// copy, paste , group, ungroup, drop, delete as the keys of the selection do them // bendset =... the bent model being edited: length, lateral, height, along, side, height // bendprobe logs the bent model being edited // undo, redo @@ -626,6 +631,67 @@ void editor_mode::selftest_step() ui()->set_node(m_node); WriteLog("SELFTEST selectmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); } + else if (command == "addmodel") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TAnimModel *nearest{nullptr}; + auto best{50.0}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr) + continue; + auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; + if (distance < best) + { + best = distance; + nearest = model; + } + } + select_node(nearest, true); + WriteLog("SELFTEST addmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); + } + else if (command == "selection") + { + std::string text; + for (auto *node : selection_with_groups()) + text += format(" [%s %.2f %.2f %.2f group %d]", node->name().c_str(), node->location().x, node->location().y, node->location().z, static_cast(node->group())); + WriteLog("SELFTEST selection:" + text); + } + else if (command == "moveselection" && m_node != nullptr) + { + glm::dvec3 offset{0.0}; + words >> offset.x >> offset.y >> offset.z; + selection_snapshots(EditorSnapshot::Action::Move); + auto const before{m_node->location()}; + m_editor.translate(m_node, before + offset, true); + selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location()); + } + else if (command == "turnselection" && m_node != nullptr) + { + float degrees{0.0f}; + words >> degrees; + selection_snapshots(EditorSnapshot::Action::Rotate); + glm::vec3 const turned{0.0f, degrees, 0.0f}; + m_editor.rotate(m_node, turned, 0.0f); + selection_follow(glm::dvec3(0.0), turned, m_node->location()); + } + else if (command == "copy") + copy_selection(); + else if (command == "paste") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.y >> point.z; + paste_clipboard(point); + } + else if (command == "group") + group_selection(); + else if (command == "ungroup") + ungroup_selection(); + else if (command == "drop") + drop_selection_to_ground(); + else if (command == "delete") + delete_selected(); else if (command == "selectinclude") { glm::dvec3 point{0.0}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 689ee15f..89d3b3e1 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -582,15 +582,31 @@ std::array const &editor_mode::track_modes() void editor_mode::render_track_menu() { - if (ImGui::MenuItem(STR_C("Tracks"), nullptr, m_track_window_open)) + // the tools of the tracks with their keys, as in the toolbar of the field of work + if (false == ImGui::BeginMenu(STR_C("Tracks"))) + return; + for (auto const &mode : track_modes()) { - if (m_track_window_open) - m_track_window_open = false; - else - show_track_tab(m_track_tab); + if (ImGui::MenuItem(STR_C(mode.label), mode.key, m_track_window_open && m_track_tab == mode.tab)) + { + terrain_workspace(false); + show_track_tab(mode.tab); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(mode.tooltip)); } + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Find a track by its name"), "Ctrl+F")) + { + terrain_workspace(false); + show_track_tab(m_track_window_open ? m_track_tab : track_tab::path); + m_track_search.focus = true; + } + if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.open)) + m_gauge.open = !m_gauge.open; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Track editor: the window with its modes opens or closes")); + ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)")); + ImGui::EndMenu(); } void editor_mode::show_track_tab(track_tab const Tab) diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index 8fc55fc9..79c265df 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -972,6 +972,8 @@ void editor_ui::render_menu_contents() } ImGui::EndMenu(); } + if (m_editmenu) + m_editmenu(); ui_layer::render_menu_contents(); diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f95af4c2..deb58bfb 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -69,6 +69,11 @@ class editor_ui : public ui_layer { m_toolbaroptions = std::move(Renderer); } + // draws the edit menu of the editor mode, next to the file menu + void set_edit_menu(std::function Renderer) + { + m_editmenu = std::move(Renderer); + } // draws the fields of work of the editor mode in a row above the toolbar void set_workspace_bar(std::function Renderer) { @@ -188,6 +193,7 @@ class editor_ui : public ui_layer std::function m_menuoptions; std::function m_toolbaroptions; std::function m_workspacebar; + std::function m_editmenu; bool m_layoutreset{false}; // the default layout is to be built again on the next frame bool m_tooloptionsopen{true}; bool m_inspectoropen{true}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index c16a20ae..bec33efc 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -4722,3 +4722,270 @@ msgstr "LPM dodaje kafel obok klikniętego, Shift+LPM go usuwa" msgid "A vehicle to drive, put on the selected track; it isn't written to the scenery files" msgstr "Pojazd do jazdy, stawiany na zaznaczonym torze; nie trafia do plików scenerii" + +msgid "%zu groups taken apart" +msgstr "Rozgrupowano grup: %zu" + +msgid "%zu models copied; Ctrl+V puts them at the cursor" +msgstr "Skopiowano modeli: %zu; Ctrl+V wstawia je przy kursorze" + +msgid "%zu models grouped: a click on one selects them all; Ctrl+Shift+G takes the group apart" +msgstr "Zgrupowano modeli: %zu; klik na jednym zaznacza wszystkie, Ctrl+Shift+G rozgrupowuje" + +msgid "%zu models pasted; Ctrl+Z takes them back" +msgstr "Wklejono modeli: %zu; Ctrl+Z cofa" + +msgid "%zu models selected" +msgstr "Zaznaczono modeli: %zu" + +msgid "%zu models selected; Shift+LMB adds or takes one out" +msgstr "Zaznaczono modeli: %zu; Shift+LPM dodaje lub odejmuje model" + +msgid "Add a model to the selection, or take it out" +msgstr "Dodaje model do zaznaczenia albo go odejmuje" + +msgid "Add a track to the selection, drag: a box" +msgstr "Dodaje tor do zaznaczenia, przeciąganie: ramka" + +msgid "Area fill: take the last point of the outline back" +msgstr "Wypełnianie obszaru: cofnij ostatni punkt obrysu" + +msgid "Bend the selected along the track" +msgstr "Wygnij zaznaczony wzdłuż toru" + +msgid "Bends the selected model, e.g. a platform edge or a fence, along the nearest track; the settings are in the inspector" +msgstr "Wygina zaznaczony model, np. krawędź peronu albo płot, wzdłuż najbliższego toru; ustawienia są w inspektorze" + +msgid "Build: straight ahead" +msgstr "Budowa: prosto przed siebie" + +msgid "Camera" +msgstr "Kamera" + +msgid "Copies the selected models with their groups" +msgstr "Kopiuje zaznaczone modele razem z ich grupami" + +msgid "Copy" +msgstr "Kopiuj" + +msgid "Copy the selected models / paste them at the cursor" +msgstr "Kopiuj zaznaczone modele / wklej je przy kursorze" + +msgid "Ctrl held" +msgstr "Ctrl wciśnięty" + +msgid "Ctrl held while dragging" +msgstr "Ctrl wciśnięty przy przeciąganiu" + +msgid "Ctrl+RMB" +msgstr "Ctrl+PPM" + +msgid "Delete the selected" +msgstr "Usuń zaznaczone" + +msgid "Delete the selected tracks" +msgstr "Usuń zaznaczone tory" + +msgid "Deletes the selected models" +msgstr "Usuwa zaznaczone modele" + +msgid "Deletes what's selected: a model, tracks, a road piece or point, a signal, an include of a scenery template" +msgstr "Usuwa to, co zaznaczone: model, tory, kawałek lub punkt drogi, sygnalizator, include szablonu scenerii" + +msgid "Drop the selected onto the ground, each model of a group by itself" +msgstr "Opuść zaznaczone na ziemię, każdy model grupy osobno" + +msgid "Drop to the ground" +msgstr "Opuść na ziemię" + +msgid "Enter / Backspace" +msgstr "Enter / Backspace" + +msgid "Fields of work" +msgstr "Tryby pracy" + +msgid "Find a track by its name" +msgstr "Znajdź tor po nazwie" + +msgid "Finish the road being built" +msgstr "Zakończ budowaną drogę" + +msgid "Fly forwards, back, left, right (arrows in the legacy scheme)" +msgstr "Lot do przodu, do tyłu, w lewo, w prawo (strzałki w starym układzie)" + +msgid "Fly to the selected" +msgstr "Leć do zaznaczonego" + +msgid "Fly up / down (Page Up / Page Down in the legacy scheme)" +msgstr "Lot w górę / w dół (Page Up / Page Down w starym układzie)" + +msgid "Group the selected models / take their group apart" +msgstr "Grupuj zaznaczone modele / rozgrupuj ich grupę" + +msgid "Help" +msgstr "Pomoc" + +msgid "Keyboard shortcuts" +msgstr "Skróty klawiszowe" + +msgid "Lay track / switch / turntable" +msgstr "Układanie toru / rozjazd / obrotnica" + +msgid "Laying track: finish / take the last point back" +msgstr "Układanie toru: zakończ / cofnij ostatni punkt" + +msgid "Leave the editor" +msgstr "Wyjście z edytora" + +msgid "Local space" +msgstr "Lokalne osie" + +msgid "Log" +msgstr "Log" + +msgid "Look around" +msgstr "Rozglądanie się" + +msgid "Makes the change taken back again" +msgstr "Przywraca cofniętą zmianę" + +msgid "Menu of the selection at the cursor" +msgstr "Menu zaznaczenia przy kursorze" + +msgid "Nothing copied yet; Ctrl+C copies the selected models" +msgstr "Nic jeszcze nie skopiowano; Ctrl+C kopiuje zaznaczone modele" + +msgid "Nothing selected" +msgstr "Nic nie zaznaczono" + +msgid "Paste" +msgstr "Wklej" + +msgid "Paste here" +msgstr "Wklej tutaj" + +msgid "Profile / speed / joints / infrastructure" +msgstr "Profil / prędkość / styki / infrastruktura" + +msgid "Puts each selected model, also each model of their groups, down onto the terrain or the object under it" +msgstr "Opuszcza każdy zaznaczony model, także każdy model ich grup, na teren albo obiekt pod nim" + +msgid "Puts the copied models at the cursor, around it as they were around the selected one; Ctrl+RMB in the view pastes them where it was clicked" +msgstr "Wstawia skopiowane modele przy kursorze, rozłożone wokół niego tak jak wokół zaznaczonego; Ctrl+PPM w widoku wkleja je w klikniętym miejscu" + +msgid "Puts the copied models here, around this point as they were around the selected one" +msgstr "Wstawia skopiowane modele tutaj, rozłożone wokół tego punktu tak jak wokół zaznaczonego" + +msgid "Quit" +msgstr "Wyjście" + +msgid "RMB held, E / Q" +msgstr "PPM wciśnięty, E / Q" + +msgid "RMB held, W S A D" +msgstr "PPM wciśnięty, W S A D" + +msgid "RMB held, mouse" +msgstr "PPM wciśnięty, mysz" + +msgid "Redo" +msgstr "Ponów" + +msgid "Save the scenery" +msgstr "Zapisz scenerię" + +msgid "Screenshot" +msgstr "Zrzut ekranu" + +msgid "Sculpt: lower; chunks: delete one" +msgstr "Rzeźbienie: obniżanie; kafle: usunięcie kafla" + +msgid "Sculpt: raise; smooth: even out; chunks: add one" +msgstr "Rzeźbienie: podnoszenie; wygładzanie: wyrównanie; kafle: dodanie kafla" + +msgid "Select a model to copy" +msgstr "Zaznacz model do skopiowania" + +msgid "Select a model, or place one in the insert and brush modes" +msgstr "Zaznacz model albo postaw go w trybach wstawiania i pędzla" + +msgid "Select tool; cancels what's going on first" +msgstr "Narzędzie zaznaczania; najpierw przerywa to, co trwa" + +msgid "Select two models or more to group them: Shift+LMB adds one to the selection" +msgstr "Zaznacz co najmniej dwa modele, by je zgrupować: Shift+LPM dodaje model do zaznaczenia" + +msgid "Select, insert, brush, area fill, copy to bank" +msgstr "Zaznaczanie, wstawianie, pędzel, wypełnianie obszaru, kopiowanie do banku" + +msgid "Select: add a point, or take it out" +msgstr "Zaznaczanie: dodaj punkt albo go odejmij" + +msgid "Signals / objects along the track" +msgstr "Sygnalizatory / obiekty wzdłuż toru" + +msgid "Snap to the step set in the gizmo settings" +msgstr "Przyciąganie do kroku z ustawień gizma" + +msgid "Split the road at the cursor, or make a point there" +msgstr "Podziel drogę przy kursorze albo zrób tam punkt" + +msgid "Split the selected track at the cursor" +msgstr "Podziel zaznaczony tor przy kursorze" + +msgid "Straight / curve" +msgstr "Prosta / łuk" + +msgid "Takes the last change back" +msgstr "Cofa ostatnią zmianę" + +msgid "The camera flies to the selected model and looks at it" +msgstr "Kamera leci do zaznaczonego modelu i na niego patrzy" + +msgid "The handles along the axes of the selected model instead of the axes of the world" +msgstr "Uchwyty wzdłuż osi zaznaczonego modelu zamiast osi świata" + +msgid "The keys of the editor, for each field of work" +msgstr "Klawisze edytora w każdym trybie pracy" + +msgid "The models of the group move each by itself again" +msgstr "Modele grupy znów poruszają się osobno" + +msgid "The selected model isn't in a group" +msgstr "Zaznaczony model nie jest w grupie" + +msgid "The selected models move together from now on; a click on one selects them all" +msgstr "Zaznaczone modele poruszają się odtąd razem; klik na jednym zaznacza wszystkie" + +msgid "The selected models move together from now on; a click on one selects them all. Shift+LMB adds a model to the selection" +msgstr "Zaznaczone modele poruszają się odtąd razem; klik na jednym zaznacza wszystkie. Shift+LPM dodaje model do zaznaczenia" + +msgid "The view straight down without perspective, as on a map; again for the camera as it was" +msgstr "Widok z góry bez perspektywy, jak na mapie; ponownie: kamera jak była" + +msgid "This list of shortcuts" +msgstr "Ta lista skrótów" + +msgid "Top view, orthographic" +msgstr "Widok z góry, ortogonalny" + +msgid "Translate / rotate / scale" +msgstr "Przesuwanie / obrót / skala" + +msgid "Undo" +msgstr "Cofnij" + +msgid "Undo / redo" +msgstr "Cofnij / ponów" + +msgid "Ungroup" +msgstr "Rozgrupuj" + +msgid "Unpin all docks for the most of the view, or pin them back" +msgstr "Odepnij wszystkie doki dla większego widoku albo przypnij je z powrotem" + +msgid "What can be done with it" +msgstr "Co można z nim zrobić" + +msgid "What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values" +msgstr "Co robią uchwyty gizma na zaznaczeniu przy przeciąganiu; z Ctrl wartości są przyciągane" diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index b6b4ae17..15197b16 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "model/AnimModel.h" +#include "scene/scenenodegroups.h" #include "world/MemCell.h" #include "world/Traction.h" #include "world/EvLaunch.h" @@ -688,6 +689,11 @@ save_result node_layers::save(std::vector const &Rootstatements, st { return true; } + if (Model != nullptr && Groups.relocating(Node)) + { + // written anew along with its group, below + return true; + } auto const location{Node->location()}; auto const angles{Model != nullptr ? Model->Angles() : glm::vec3{0.f}}; auto const scale{Model != nullptr ? Model->Scale() : glm::vec3{1.f}}; @@ -806,6 +812,87 @@ save_result node_layers::save(std::vector const &Rootstatements, st return fail(state.error); } } + // models grouped or ungrouped in the editor: their definitions are taken out of where they are and written anew, a group + // as one block between group and endgroup in the layer of its first model, a model of a dissolved group on its own + std::set relocated; + auto const relocate = [&](TAnimModel const *Model, layer_handle const Target, std::vector> &Output) { + if (auto const lookup{m_sources.find(Model)}; lookup != m_sources.end()) + { + auto const &source{lookup->second}; + if (false == writable(source.layer)) + { + // one definition stands for more than one node, it stays where it is + return true; + } + if (false == load(source.layer)) + { + return false; + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(Model); + } + std::string text; + Model->export_as_text(text); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + auto const &context{layer(Target).context_insert()}; + if (false == context.matches(layer_context())) + { + model_placement placement; + placement.rotation = context.rotation; + placement.location = context.to_local(Model->location()); + placement.angles = Model->Angles(); + placement.scale = Model->Scale() / context.scale; + std::string error; + if (false == patch_model(text, placement, error)) + { + state.error = error + " of \"" + Model->name() + "\""; + return false; + } + } + Output.emplace_back(Model, std::move(text)); + relocated.emplace(Model); + return true; + }; + for (auto *node : Groups.loose()) + { + auto const *model{dynamic_cast(node)}; + if (model == nullptr || model->from_template() || false == is_output(resolve(node->layer()))) + { + continue; + } + if (false == relocate(model, resolve(node->layer()), created[resolve(node->layer())])) + { + return fail(state.error); + } + } + for (auto const group : Groups.rewritten()) + { + auto const &nodes{Groups.group(group).nodes}; + if (nodes.empty() || false == is_output(resolve(nodes.front()->layer()))) + { + continue; + } + auto const target{resolve(nodes.front()->layer())}; + auto &output{created[target]}; + // NOTE: lines with no node are written as they are, without a place in the bookkeeping + output.emplace_back(nullptr, "group"); + for (auto const *node : nodes) + { + auto const *model{dynamic_cast(node)}; + if (model == nullptr || model->from_template()) + { + continue; + } + if (false == relocate(model, target, output)) + { + return fail(state.error); + } + } + output.emplace_back(nullptr, "endgroup"); + } auto const push_edit = [&](layer_handle const Layer, source_span const &Span, std::string Text, basic_node const *Node) { file_patch::edit edit; edit.begin = Span.begin; @@ -1448,7 +1535,10 @@ save_result node_layers::save(std::vector const &Rootstatements, st { ensure_newline(added.text, eol); auto const offset{static_cast(added.text.size())}; - added.nodes.push_back({definition.first, {offset, offset + static_cast(definition.second.size())}, true}); + if (definition.first != nullptr) + { + added.nodes.push_back({definition.first, {offset, offset + static_cast(definition.second.size())}, true}); + } added.text += definition.second + eol; } } @@ -1620,6 +1710,7 @@ save_result node_layers::save(std::vector const &Rootstatements, st } // the scenery files now match the scene, update the bookkeeping to reflect it + Groups.saved(); auto removedterrain{false}; for (auto const output : outputs) { @@ -1629,7 +1720,8 @@ save_result node_layers::save(std::vector const &Rootstatements, st auto const isnew{source.layer == null_handle}; source.layer = output; source.span = place.span; - if (isnew) + // a definition written anew elsewhere is in the placement of the place it went to, as a new one is + if (isnew || relocated.count(place.node) != 0) { source.context = layer(output).context_insert(); } diff --git a/scene/scenenodegroups.cpp b/scene/scenenodegroups.cpp index 3ed357f3..78128780 100644 --- a/scene/scenenodegroups.cpp +++ b/scene/scenenodegroups.cpp @@ -22,9 +22,20 @@ node_groups Groups; // requests creation of a new node group. returns: handle to the group scene::group_handle -node_groups::create() { +node_groups::create( bool const Separate ) { - m_activegroup.push( create_handle() ); + if( false == Separate ) { + m_activegroup.push( create_handle() ); + return handle(); + } + // a handle no other group has, see make() + auto group { std::max( 2, static_cast( m_groupmap.size() + 1 ) ) }; + while( m_groupmap.find( group ) != m_groupmap.end() ) { + ++group; + } + // NOTE: registered before the nodes come, so that a group nested in this one doesn't get the same handle + m_groupmap[ group ]; + m_activegroup.push( group ); return handle(); } @@ -334,6 +345,87 @@ node_groups::erase( group_map::const_iterator Group ) { m_groupmap.erase( Group ); } +group_handle +node_groups::make( std::vector const &Nodes ) { + + // a handle no other group has, past the first one, which the editor treats as no group at all + auto handle { std::max( 2, static_cast( m_groupmap.size() + 1 ) ) }; + while( m_groupmap.find( handle ) != m_groupmap.end() ) { + ++handle; + } + for( auto *node : Nodes ) { + leave( node ); + m_loose.erase( node ); + insert( handle, node ); + } + m_rewrite.insert( handle ); + return handle; +} + +void +node_groups::dissolve( scene::group_handle const Group ) { + + auto const lookup { m_groupmap.find( Group ) }; + if( lookup == m_groupmap.end() ) { return; } + for( auto *node : lookup->second.nodes ) { + m_loose.insert( node ); + } + m_rewrite.erase( Group ); + erase( lookup ); +} + +void +node_groups::remove( scene::basic_node *Node ) { + + leave( Node ); + m_loose.erase( Node ); +} + +void +node_groups::leave( scene::basic_node *Node ) { + + auto const group { Node->group() }; + Node->group( null_handle ); + auto const lookup { m_groupmap.find( group ) }; + if( lookup == m_groupmap.end() ) { return; } + auto &nodes { lookup->second.nodes }; + nodes.erase( std::remove( std::begin( nodes ), std::end( nodes ), Node ), std::end( nodes ) ); + if( nodes.empty() && lookup->second.events.empty() ) { + // a group made in the editor goes with its last node + m_rewrite.erase( group ); + m_groupmap.erase( lookup ); + } +} + +bool +node_groups::relocating( scene::basic_node const *Node ) const { + + return ( m_loose.count( Node ) != 0 ) || ( m_rewrite.count( Node->group() ) != 0 ); +} + +std::vector +node_groups::rewritten() const { + + return { std::begin( m_rewrite ), std::end( m_rewrite ) }; +} + +std::vector +node_groups::loose() const { + + std::vector nodes; + for( auto const *node : m_loose ) { + nodes.emplace_back( const_cast( node ) ); + } + return nodes; +} + +void +node_groups::saved() { + + m_rewrite.clear(); + m_loose.clear(); +} + // creates handle for a new group group_handle node_groups::create_handle() { diff --git a/scene/scenenodegroups.h b/scene/scenenodegroups.h index 350fa3d3..fb7ea95f 100644 --- a/scene/scenenodegroups.h +++ b/scene/scenenodegroups.h @@ -29,8 +29,9 @@ public: node_groups() = default; // methods // requests creation of a new node group. returns: handle to the group + // nested groups are flattened into the outer one, unless Separate: then the group stands by itself (group ... endgroup) group_handle - create(); + create( bool const Separate = false ); // indicates creation of current group ended. returns: handle to the parent group or null_handle if group stack is empty group_handle close(); @@ -50,6 +51,26 @@ public: scene::basic_group & group( scene::group_handle const Group ) { return m_groupmap[ Group ]; } + // groups made in the scenery editor. the nodes leave the groups they were in. returns: handle to the new group + group_handle + make( std::vector const &Nodes ); + // the nodes of the group are left on their own + void + dissolve( scene::group_handle const Group ); + // the node, about to be deleted, leaves its group + void + remove( scene::basic_node *Node ); + // groups made or dissolved in the editor are written anew when the scenery is saved: the definitions of their nodes are + // taken out of where they are, a group's written as one block between group and endgroup, a dissolved one's nodes each on its own + bool + relocating( scene::basic_node const *Node ) const; + std::vector + rewritten() const; + std::vector + loose() const; + // the scenery files match the groups now + void + saved(); // sends basic content of the class in legacy (text) format to provided stream void export_as_text( std::ostream &Output, bool const Dirty ) const; @@ -66,9 +87,14 @@ private: create_handle(); bool assign_cross_switch(map::track_switch&sw, std::string &sw_name, const std::string &id, size_t idx); + // the node leaves the group it's in + void + leave( scene::basic_node *Node ); // members group_map m_groupmap; // map of established node groups std::stack m_activegroup; // helper, group to be assigned to newly created nodes + std::set m_rewrite; // groups made in the editor, to be written as blocks on save + std::set m_loose; // nodes of groups dissolved in the editor, to be written on their own on save }; extern node_groups Groups; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index f73f913c..2c80eada 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -471,7 +471,8 @@ void state_serializer::init_time() { void state_serializer::deserialize_group( cParser &Input, scene::scratch_data &Scratchpad ) { - scene::Groups.create(); + // a group of the scenery stands by itself, rather than being a part of the file it's in (e.g. made in the editor) + scene::Groups.create( true ); } void