17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-18 07:59:19 +02:00

feat(editor): more models selected at once (Shift+LMB), moved and turned by the gizmo together, undone in one step; Ctrl+C / Ctrl+V copy the selected models with their groups and paste them at the cursor; Ctrl+G groups the selection, Ctrl+Shift+G takes a group apart, groups written to the scenery as group ... endgroup and loaded as groups of their own; End drops each model of the selection and of their groups onto the ground; menu of the selection at the cursor on Ctrl+RMB; Edit menu with undo, redo, the clipboard, grouping, deleting; Help > Keyboard shortcuts (F1) with the keys of every field of work, F2-F5 choose the fields of work, the tracks menu lists the track tools with their keys, the keys and descriptions in the menus and tooltips; self-test commands for the selection; Polish translation

This commit is contained in:
2026-10-09 01:12:46 +02:00
parent 791d5d052c
commit f94100ffe7
14 changed files with 1354 additions and 78 deletions

View File

@@ -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"

View File

@@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/.
#include <fstream>
#include <regex>
#include "application/editormode.h"
#include "scene/scenenodegroups.h"
#include "application/editoruilayer.h"
#ifndef _WIN32
#include <spawn.h>
@@ -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<TAnimModel *>(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<scene::basic_node *> 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<scene::basic_node *> 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<int>(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<TAnimModel *>(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<shortcut> shortcuts;
};
static std::vector<group> 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<TAnimModel *>(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<TAnimModel *>(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<TAnimModel *>(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<double>(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))

View File

@@ -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<stored_profile> profile_library;
std::vector<array_copy> 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<scene::basic_node *> m_selection;
std::vector<scene::basic_node *> selected_nodes() const; // m_node first
std::vector<scene::basic_node *> 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<clipboard_entry> 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();

View File

@@ -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();
}
}

View File

@@ -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 <glm/gtx/rotate_vector.hpp>
#include <algorithm>
#include <map>
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<TAnimModel *>(Node) : nullptr;
}
} // namespace
std::vector<scene::basic_node *> editor_mode::selected_nodes() const
{
std::vector<scene::basic_node *> 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<scene::basic_node *> 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<scene::group_handle> 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<double>(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<scene::group_handle, int> 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<int>(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<TAnimModel *> made;
std::map<int, std::vector<scene::basic_node *>> 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<scene::group_handle> 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();
}

View File

@@ -53,6 +53,11 @@ http://mozilla.org/MPL/2.0/.
// middle, point), height (origin, bottom, top, edge, point), point=<x>,<y>,<z>,
// pick=<x>,<y>,<z> (a point of the scene picked as the reference point), go (0, 1)
// selectinclude <x> <z> the include placed nearest the point
// addmodel <x> <z> 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 <x> <y> <z> the selection moved by the offset, as the gizmo moves it
// turnselection <degrees> the selection turned around the model with the gizmo
// copy, paste <x> <y> <z>, group, ungroup, drop, delete as the keys of the selection do them
// bendset <key>=<value>... 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<int>(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};

View File

@@ -582,15 +582,31 @@ std::array<editor_mode::track_mode, 12> 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)

View File

@@ -972,6 +972,8 @@ void editor_ui::render_menu_contents()
}
ImGui::EndMenu();
}
if (m_editmenu)
m_editmenu();
ui_layer::render_menu_contents();

View File

@@ -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<void()> 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<void()> Renderer)
{
@@ -188,6 +193,7 @@ class editor_ui : public ui_layer
std::function<void()> m_menuoptions;
std::function<void()> m_toolbaroptions;
std::function<void()> m_workspacebar;
std::function<void()> 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};

View File

@@ -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"

View File

@@ -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<std::string> 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<std::string> 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<basic_node const *> relocated;
auto const relocate = [&](TAnimModel const *Model, layer_handle const Target, std::vector<std::pair<basic_node const *, std::string>> &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<TAnimModel const *>(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<TAnimModel const *>(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<std::string> const &Rootstatements, st
{
ensure_newline(added.text, eol);
auto const offset{static_cast<std::streamoff>(added.text.size())};
added.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
if (definition.first != nullptr)
{
added.nodes.push_back({definition.first, {offset, offset + static_cast<std::streamoff>(definition.second.size())}, true});
}
added.text += definition.second + eol;
}
}
@@ -1620,6 +1710,7 @@ save_result node_layers::save(std::vector<std::string> 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<std::string> 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();
}

View File

@@ -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<group_handle>( 2, static_cast<group_handle>( 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<scene::basic_node *> 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<group_handle>( 2, static_cast<group_handle>( 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<scene::group_handle>
node_groups::rewritten() const {
return { std::begin( m_rewrite ), std::end( m_rewrite ) };
}
std::vector<scene::basic_node *>
node_groups::loose() const {
std::vector<scene::basic_node *> nodes;
for( auto const *node : m_loose ) {
nodes.emplace_back( const_cast<scene::basic_node *>( 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() {

View File

@@ -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<scene::basic_node *> 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<scene::group_handle>
rewritten() const;
std::vector<scene::basic_node *>
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<scene::group_handle> m_activegroup; // helper, group to be assigned to newly created nodes
std::set<scene::group_handle> m_rewrite; // groups made in the editor, to be written as blocks on save
std::set<scene::basic_node const *> m_loose; // nodes of groups dissolved in the editor, to be written on their own on save
};
extern node_groups Groups;

View File

@@ -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