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

feat(editor): icons over the 3d view: the tools of the field of work along its left edge (the edit modes of the surroundings, the track tools, the road tools with the lanes, the terrain tools with the orthophoto), the modes of the gizmo at its top, the axes of the world as the camera sees them in its top right corner, a click on one looking along it, with the top view under them; the icons drawn by editor/icons/editor_icons.py into a png which CMake writes out as an array built into the executable; the gizmo modes and the top view left out of the toolbar; Polish translation

This commit is contained in:
2026-10-09 11:54:47 +02:00
parent f2e73812d9
commit 645a54a739
9 changed files with 1049 additions and 23 deletions

View File

@@ -0,0 +1,398 @@
/*
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/.
*/
// over the 3d view: the tools of the field of work along its left edge, the modes of the gizmo at its top, the axes of the
// world and the top view in its top right corner
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "editor/editorIcons.hpp"
#include "rendering/renderer.h"
#include "utilities/Globals.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <vector>
namespace
{
using editor_icons::icon;
struct tool_button
{
icon picture;
char const *label;
char const *key; // nullptr if there's none
char const *tooltip; // nullptr if the label says enough
bool chosen;
std::function<void()> action;
bool group{false}; // a gap before it, it starts another group
bool enabled{true};
};
float scale()
{
return std::max(1.0f, Global.ui_scale);
}
// side of a button, the icon with some room around it
float button_side()
{
return editor_icons::size() + 10.0f * scale();
}
// a window of no looks of its own, for the buttons over the 3d view; it takes the mouse only where it lies
bool begin_overlay(char const *Name, ImVec2 const &Position, ImVec2 const &Size)
{
ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID);
ImGui::SetNextWindowPos(Position);
ImGui::SetNextWindowSize(Size);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(1.0f, 1.0f));
auto const flags{ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollWithMouse};
auto const open{ImGui::Begin(Name, nullptr, flags)};
ImGui::PopStyleVar(3);
return open;
}
// the button with the icon at the place; the label stands in for the icon if the renderer can't show images
void icon_button(tool_button const &Button, ImVec2 const &Position, int const Id)
{
auto const side{button_side()};
ImGui::SetCursorScreenPos(Position);
ImGui::PushID(Id);
auto const clicked{ImGui::InvisibleButton("##tool", ImVec2(side, side))};
ImGui::PopID();
auto const hovered{ImGui::IsItemHovered()};
auto *list{ImGui::GetWindowDrawList()};
ImVec2 const end{Position.x + side, Position.y + side};
auto const rounding{4.0f * scale()};
auto const background{Button.chosen ? ImGui::GetColorU32(ImGuiCol_ButtonActive) :
hovered && Button.enabled ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) :
IM_COL32(24, 26, 30, 200)};
list->AddRectFilled(Position, end, background, rounding);
if (Button.chosen)
list->AddRect(Position, end, ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, 0, 1.5f * scale());
auto const color{Button.enabled ? (Button.chosen || hovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(215, 220, 225, 235)) : IM_COL32(150, 150, 150, 110)};
ImVec2 const center{Position.x + side * 0.5f, Position.y + side * 0.5f};
if (false == editor_icons::draw(list, Button.picture, center, color))
{
char const text[]{Button.label[0], (Button.label[0] != '\0' ? Button.label[1] : '\0'), '\0'};
auto const size{ImGui::CalcTextSize(text)};
list->AddText(ImVec2(center.x - size.x * 0.5f, center.y - size.y * 0.5f), color, text);
}
if (hovered)
{
std::string tip{Button.label};
if (Button.key != nullptr)
tip += std::string{" ["} + Button.key + "]";
if (Button.tooltip != nullptr)
tip += std::string{"\n"} + Button.tooltip;
ImGui::SetTooltip("%s", tip.c_str());
}
if (clicked && Button.enabled && Button.action)
Button.action();
}
} // namespace
void editor_mode::render_viewport_overlays()
{
auto const width{editor_ui::view_max().x - editor_ui::view_min().x};
auto const height{editor_ui::view_max().y - editor_ui::view_min().y};
// a view squeezed between the docks has no room for them
if (width < 8.0f * button_side() || height < 6.0f * button_side())
return;
render_viewport_tools();
render_viewport_gizmo();
render_view_axes();
}
void editor_mode::render_viewport_tools()
{
// the tools of the field of work, as in the toolbar
std::vector<tool_button> buttons;
buttons.reserve(16);
auto const area{current_work_area()};
switch (area)
{
case work_area::surroundings:
{
struct mode
{
icon picture;
char const *label;
char const *key;
nodebank_panel::edit_mode which;
};
mode const modes[] = {{icon::select, STR_C("Select"), "1", nodebank_panel::MODIFY},
{icon::insert, STR_C("Insert"), "2", nodebank_panel::ADD},
{icon::brush, STR_C("Brush"), "3", nodebank_panel::BRUSH},
{icon::area_fill, STR_C("Area fill"), "4", nodebank_panel::FILL},
{icon::copy_to_bank, STR_C("Copy to bank"), "5", nodebank_panel::COPY}};
for (auto const &entry : modes)
buttons.push_back({entry.picture, entry.label, entry.key, nullptr, ui()->mode() == entry.which, [this, which = entry.which]() { choose_edit_mode(which); }});
break;
}
case work_area::tracks:
{
auto const vehicle{m_track_tab == track_tab::lineside && m_vehicle.open};
auto const add = [&](track_tab const Tab, icon const Picture, bool const Group) {
auto const &modes{track_modes()};
auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == Tab; })};
if (found == modes.end())
return;
// a tool chosen again gives way to the select one, as in the toolbar
auto const chosen{m_track_tab == Tab && false == (Tab == track_tab::lineside && vehicle)};
buttons.push_back({Picture, STR_C(found->label), found->key, STR_C(found->tooltip), chosen, [this, Tab, chosen]() { show_track_tab(chosen ? track_tab::path : Tab); }, Group});
};
add(track_tab::path, icon::select, false);
add(track_tab::lay, icon::lay_track, false);
add(track_tab::turnout, icon::turnout, false);
add(track_tab::straights, icon::straight, false);
add(track_tab::route, icon::curve, false);
add(track_tab::turntable, icon::turntable, false);
add(track_tab::signals, icon::signal, true);
add(track_tab::lineside, icon::objects, false);
buttons.push_back({icon::vehicle, STR_C("Vehicle"), nullptr, STR_C("A vehicle to drive, put on the selected track; it isn't written to the scenery files"), vehicle, [this, vehicle]() {
show_track_tab(vehicle ? track_tab::path : track_tab::lineside);
m_vehicle.expand = false == vehicle;
}});
add(track_tab::profile, icon::profile, true);
add(track_tab::speed, icon::speed, false);
add(track_tab::joints, icon::joints, false);
add(track_tab::infra, icon::infra, false);
buttons.push_back({icon::gauge, STR_C("Structure gauge"), nullptr, STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"), m_gauge.open,
[this]() { m_gauge.open = !m_gauge.open; }});
break;
}
case work_area::roads:
{
auto &tool{m_roadtool};
buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB: a road piece, a junction, a point where pieces meet, a level crossing or a traffic point"), tool.tool == 0, [this]() { road_choose_tool(0); }});
buttons.push_back({icon::road, STR_C("Build"), nullptr, STR_C("LMB: start a road, then each next point; on a loose end, the side of a road or a junction it's joined to them"), tool.tool == 1, [this]() { road_choose_tool(1); }});
buttons.push_back({icon::place, STR_C("Place"), nullptr, STR_C("Level crossings, and the points where vehicles appear on the roads or are taken off them"), tool.tool == 2, [this]() { road_choose_tool(2); }});
buttons.push_back({icon::lanes, STR_C("Show lanes"), nullptr, STR_C("The lanes of the roads drawn as the paths the vehicles take"), tool.lanes, [&tool]() { tool.lanes = !tool.lanes; }, true});
break;
}
case work_area::terrain:
{
// what the left button does on the terrain, as in the toolbar
auto const choose = [this](bool const Sculpt, bool const Smooth, bool const Chunks) {
return [this, Sculpt, Smooth, Chunks]() {
m_terrain_sculpt = Sculpt;
m_terrain_brush_smooth = Smooth;
m_chunk_edit = Chunks;
};
};
buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), false == m_terrain_sculpt && false == m_chunk_edit, choose(false, m_terrain_brush_smooth, false)});
buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), m_terrain_sculpt && false == m_terrain_brush_smooth, choose(true, false, false)});
buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), m_terrain_sculpt && m_terrain_brush_smooth, choose(true, true, false)});
buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB deletes it"), m_chunk_edit, choose(false, m_terrain_brush_smooth, true)});
auto const orthophoto{m_orthophoto.enabled()};
buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto,
[this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true});
break;
}
}
if (buttons.empty())
return;
// down the left edge, in more columns if the view is too low for them; the bottom is left to the hints of the tools
auto const side{button_side()};
auto const gap{2.0f * scale()};
auto const groupgap{8.0f * scale()};
auto const margin{8.0f * scale()};
ImVec2 const origin{editor_ui::view_min().x + margin, editor_ui::view_min().y + margin};
auto const bottom{std::max(origin.y + 4.0f * side, editor_ui::view_max().y - 130.0f * scale())};
std::vector<ImVec2> places;
places.reserve(buttons.size());
ImVec2 place{origin};
for (auto const &button : buttons)
{
if (button.group && place.y > origin.y)
place.y += groupgap - gap;
if (place.y > origin.y && place.y + side > bottom)
place = ImVec2(place.x + side + gap, origin.y);
places.push_back(place);
place.y += side + gap;
}
ImVec2 extent{0.0f, 0.0f};
for (auto const &at : places)
extent = ImVec2(std::max(extent.x, at.x + side - origin.x), std::max(extent.y, at.y + side - origin.y));
auto const pad{3.0f * scale()};
if (begin_overlay("##viewporttools", ImVec2(origin.x - pad, origin.y - pad), ImVec2(extent.x + 2.0f * pad, extent.y + 2.0f * pad)))
{
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(origin.x - pad, origin.y - pad), ImVec2(origin.x + extent.x + pad, origin.y + extent.y + pad), IM_COL32(16, 18, 21, 140), 6.0f * scale());
for (std::size_t index = 0; index < buttons.size(); ++index)
icon_button(buttons[index], places[index], static_cast<int>(index));
}
ImGui::End();
}
void editor_mode::render_viewport_gizmo()
{
// the gizmo moves the models and the tracks; on the terrain the mouse is the brush's
if (false == m_gizmo_enabled || current_work_area() == work_area::terrain)
return;
auto const tooltip{STR_C("What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values")};
std::array<tool_button, 4> const buttons{{
{icon::translate, STR_C("Translate"), "Q", tooltip, m_gizmo_op == gizmo_operation::translate, [this]() { m_gizmo_op = gizmo_operation::translate; }},
{icon::rotate, STR_C("Rotate"), "W", tooltip, m_gizmo_op == gizmo_operation::rotate, [this]() { m_gizmo_op = gizmo_operation::rotate; }},
{icon::scale, STR_C("Scale"), "E", tooltip, m_gizmo_op == gizmo_operation::scale, [this]() { m_gizmo_op = gizmo_operation::scale; }},
// the gizmo always scales along the axes of the model
{icon::local_space, STR_C("Local space"), "R", STR_C("The handles along the axes of the selected model instead of the axes of the world"), m_gizmo_local || m_gizmo_op == gizmo_operation::scale,
[this]() { m_gizmo_local = !m_gizmo_local; }, true, m_gizmo_op != gizmo_operation::scale},
}};
// in a row at the top, in the middle
auto const side{button_side()};
auto const gap{2.0f * scale()};
auto const groupgap{8.0f * scale()};
auto const margin{8.0f * scale()};
auto const pad{3.0f * scale()};
auto const width{4.0f * side + 2.0f * gap + groupgap};
ImVec2 const origin{std::round((editor_ui::view_min().x + editor_ui::view_max().x - width) * 0.5f), editor_ui::view_min().y + margin};
if (begin_overlay("##viewportgizmo", ImVec2(origin.x - pad, origin.y - pad), ImVec2(width + 2.0f * pad, side + 2.0f * pad)))
{
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(origin.x - pad, origin.y - pad), ImVec2(origin.x + width + pad, origin.y + side + pad), IM_COL32(16, 18, 21, 140), 6.0f * scale());
auto x{origin.x};
for (std::size_t index = 0; index < buttons.size(); ++index)
{
if (buttons[index].group)
x += groupgap - gap;
icon_button(buttons[index], ImVec2(x, origin.y), static_cast<int>(index));
x += side + gap;
}
}
ImGui::End();
}
void editor_mode::render_view_axes()
{
// the axes of the world as the camera sees them, as in blender: a click on one looks along it
auto const s{scale()};
auto const radius{40.0f * s};
auto const margin{8.0f * s};
auto const side{button_side()};
auto const size{2.0f * radius};
ImVec2 const origin{editor_ui::view_max().x - margin - size, editor_ui::view_min().y + margin};
if (begin_overlay("##viewaxes", origin, ImVec2(size, size + 6.0f * s + side)))
{
auto *list{ImGui::GetWindowDrawList()};
ImVec2 const center{origin.x + radius, origin.y + radius};
ImGui::SetCursorScreenPos(origin);
ImGui::InvisibleButton("##axes", ImVec2(size, size));
auto const hovered{ImGui::IsItemHovered()};
auto const clicked{ImGui::IsItemClicked(ImGuiMouseButton_Left)};
if (hovered)
list->AddCircleFilled(center, radius, IM_COL32(255, 255, 255, 28), 48);
// the view matrix is of the camera turned only, the axes go through it as they are
glm::mat3 const view{GfxRenderer->Camera_View_Matrix()};
struct end
{
int axis;
bool positive;
glm::vec3 seen;
};
std::array<end, 6> ends;
for (int axis = 0; axis < 3; ++axis)
{
glm::vec3 direction{0.0f};
direction[axis] = 1.0f;
auto const seen{view * direction};
ends[axis * 2] = {axis, true, seen};
ends[axis * 2 + 1] = {axis, false, -seen};
}
// the farther ones first, the nearer ones over them
std::sort(ends.begin(), ends.end(), [](end const &Left, end const &Right) { return Left.seen.z < Right.seen.z; });
ImU32 const colors[]{IM_COL32(230, 70, 80, 255), IM_COL32(130, 200, 40, 255), IM_COL32(60, 135, 235, 255)};
char const *const names[]{"X", "Y", "Z"};
auto const reach{radius - 10.0f * s};
auto const mouse{ImGui::GetIO().MousePos};
end const *pointed{nullptr};
auto const place = [&](end const &End) { return ImVec2(center.x + End.seen.x * reach, center.y - End.seen.y * reach); };
for (auto const &entry : ends)
{
auto const at{place(entry)};
auto const dot{(entry.positive ? 8.0f : 6.0f) * s};
if (hovered && (mouse.x - at.x) * (mouse.x - at.x) + (mouse.y - at.y) * (mouse.y - at.y) <= (dot + 2.0f * s) * (dot + 2.0f * s))
pointed = &entry; // the nearest one under the mouse, as it's drawn over the others
}
for (auto const &entry : ends)
{
auto const at{place(entry)};
auto const color{colors[entry.axis]};
if (entry.positive)
{
list->AddLine(center, at, color, 2.0f * s);
list->AddCircleFilled(at, 8.0f * s, color, 24);
auto const text{ImGui::CalcTextSize(names[entry.axis])};
list->AddText(ImVec2(at.x - text.x * 0.5f, at.y - text.y * 0.5f), IM_COL32(20, 20, 20, 255), names[entry.axis]);
}
else
{
list->AddCircleFilled(at, 6.0f * s, (color & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 90), 24);
list->AddCircle(at, 6.0f * s, color, 24, 1.5f * s);
}
if (&entry == pointed)
list->AddCircle(at, (entry.positive ? 9.5f : 7.5f) * s, IM_COL32(255, 255, 255, 255), 24, 1.5f * s);
}
if (pointed != nullptr)
{
ImGui::SetTooltip(STR_C("Look along %s%s; from over the scenery it's the top view"), pointed->positive ? "-" : "+", names[pointed->axis]);
if (clicked)
look_along_axis(pointed->axis, pointed->positive);
}
// the top view, under the axes
tool_button const ortho{Global.EditorOrtho ? icon::ortho : icon::perspective,
STR_C("Top view, orthographic"),
"O",
STR_C("The view straight down without perspective, as on a map; again for the camera as it was"),
Global.EditorOrtho,
[this]() { toggle_ortho(); }};
icon_button(ortho, ImVec2(std::round(center.x - side * 0.5f), origin.y + size + 6.0f * s), 0);
}
ImGui::End();
}
void editor_mode::look_along_axis(int const Axis, bool const Positive)
{
m_focus_active = false;
if (Axis == 1)
{
if (Positive)
{
// from over the scenery: the top view
if (false == Global.EditorOrtho)
toggle_ortho();
return;
}
if (Global.EditorOrtho)
toggle_ortho();
Camera.Angle.x = M_PI_2;
Camera.Angle.z = 0.0;
return;
}
if (Global.EditorOrtho)
toggle_ortho();
// from the side of the axis the camera's on, towards the other one; the yaw as the camera works it out of where it looks
glm::dvec3 where{0.0};
where[Axis] = Positive ? -1.0 : 1.0;
Camera.Angle.y = std::atan2(-where.x, -where.z);
Camera.Angle.x = 0.0;
Camera.Angle.z = 0.0;
}