mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-20 18:34:53 +02:00
Labels: - every button, field, tooltip and message of the editor reread for a beginner: short nouns for the fields with the sentence in the tooltip, the less used options under More settings, one glossary (field of work, template, track, grade), sides told from directions, degrees and per mille as signs, messages built from one format string - plural forms through STR_P; the last entry of a po file is read too, and the Polish plural rule is in the header - lang/editor_pl.po: the code pass entries merged, 391 entries no code uses and 3 duplicates dropped - roads: the notes and errors naming a road translated; the build options and the layout fold the texture length, kerb sizes, traffic share, roundabout part, height over the ground and junction speed limit under More settings, the lanes start folded Grade lines fitted to the terrain model (NMT), a line or the whole scenery at once: - fit_ground: a robust fit on nodes 5 m apart; the ground parting from the line (a bridge, a viaduct, a wall) is gone past rather than followed, the bends keep off the switches as far as their curves reach, a run of bends the same way is one point of intersection, a curve with no room is as sharp as the room lets it be - the ends of the line and the other paths of its switches leading into lines fitted already bound the line by what the tracks between can climb; ends meeting in the plan, laid apart in height by hand, are joined before the run - apply_profile: the paths are split where split_path takes the parameter (a straight was cut in the wrong place, into pieces of a few centimetres the game takes for zero length), no piece shorter than the margin; the paths off the ends of the route are brought to its height - Bytom: 1600 lines, grade breaks at the joints 89 -> 20; Tarnowskie Gory: 848 lines, 223 -> 3; no step over 2 cm
365 lines
12 KiB
C++
365 lines
12 KiB
C++
/*
|
|
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))
|
|
{
|
|
if (reason.empty())
|
|
reason = STR("it's a part of a scenery template");
|
|
ui()->set_status(format(STR_C("\"%s\" can't be grouped: %s"), node->name().empty() ? STR_C("(no name)") : node->name().c_str(), reason.c_str()), 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() ? STR_C("(no name)") : 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 together"), "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"), "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();
|
|
}
|