17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03: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

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