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:
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
362
application/editormodeselection.cpp
Normal file
362
application/editormodeselection.cpp
Normal 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();
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -972,6 +972,8 @@ void editor_ui::render_menu_contents()
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (m_editmenu)
|
||||
m_editmenu();
|
||||
|
||||
ui_layer::render_menu_contents();
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user