17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 19:24:54 +01:00
Files
maszyna/application/editormodeviewport.cpp
maj00r bbb3e9e61e feat(editor): laying and editing the track as a beginner meets them: the points clicked and not laid yet named at the top of the panel with Lay (Enter) and Drop the points, Enter shown by the bar at the bottom while there are some, and a first Esc over them only warning, the second one dropping them; the readout by the cursor kept in the 3d view, on the other side of the cursor where the view ends, with only the first warning of a curve and how many more the panel lists; the grid, the labels and the lines drawn over the view no longer seen through the docks; the gizmo in the tracks hidden until Q, W or E or its buttons above the view bring it out, an arrow there hiding it again, so a selected track isn't moved by chance; the radius of the curves at the bends headed so in the panel, the laid line told in metres and pieces, the hints of the select tool and of a straight shifted around an obstacle translated. An empty scenery opens facing north, the top view with the north up as the compass and the orthophoto have it (on LK2 Warszawa Wschodnia left, Rembertow right). The tutorial tells the laying as it works now: the bends clicked and Enter, the curves made at them and orange where too sharp for the speed, Tab for a curve traced, the radius by Ctrl+wheel when joining, the gizmo by Q W E, the terrain model loaded and fitted from the panel of the tracks, signals by I; its texts wrap to a dock it's put in, new clips of laying and of the curves, recorded with the selftest badge drawing the mouse or the key in use; the empty scenery and the shortcuts say the same
2026-10-25 11:02:45 +01:00

575 lines
28 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/.
*/
// 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. the same tools in the toolbar under the menu, with their names
#include "stdafx.h"
#include "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editormodeshared.h"
#include "editor/editorFormat.hpp"
#include "editor/editorIcons.hpp"
#include "editor/editorSettings.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>
struct editor_tool_button
{
editor_icons::icon picture; // icon::count if it has none: then it's only in the toolbar, as its name
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};
ImU32 accent{0}; // the colour of the family of the tool, 0 if it has none
};
namespace
{
using editor_icons::icon;
using tool_button = editor_tool_button;
float scale()
{
return std::max(1.0f, Global.ui_scale);
}
bool terrain_tools_more{false};
// 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 name of the tool with its key, and what it does
void button_tooltip(tool_button const &Button)
{
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());
}
// the first letters of the label in place of the icon, if the renderer can't show images
void icon_or_letters(ImDrawList *List, tool_button const &Button, ImVec2 const &Center, ImU32 const Color)
{
if (editor_icons::draw(List, Button.picture, Center, Color))
return;
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);
}
// 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)};
icon_or_letters(list, Button, ImVec2(Position.x + side * 0.5f, Position.y + side * 0.5f), color);
if (hovered)
button_tooltip(Button);
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_gizmo();
render_view_axes();
}
void editor_mode::work_area_tools(std::vector<tool_button> &Buttons)
{
switch (current_work_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.tab().open};
auto const add = [&](track_tab const Tab, 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
auto const chosen{(m_track_tab == Tab || (Tab == track_tab::straights && track_plan_tab())) && false == (Tab == track_tab::lineside && vehicle)};
Buttons.push_back({found->picture, STR_C(found->label), found->key, STR_C(found->tooltip), chosen, [this, Tab, chosen]() { show_track_tab(chosen ? track_tab::path : Tab); }, Group});
Buttons.back().accent = family_colour(found->family);
};
// the tools of laying track first, the others behind More, shown while one of them is in use
add(track_tab::path, false);
add(track_tab::lay, false);
add(track_tab::straights, false);
add(track_tab::turnout, false);
add(track_tab::parallel, false);
auto const advanced{m_track_tab == track_tab::turntable || m_track_tab == track_tab::srk || m_track_tab == track_tab::kilometrage || m_track_tab == track_tab::lineside || m_track_tab == track_tab::profile || m_track_tab == track_tab::speed ||
m_track_tab == track_tab::joints || m_gauge.is_open()};
auto const more{EditorSettings.track_tools_more()};
Buttons.push_back({icon::more, more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Turntable, signalling and signals, kilometrage, objects, trainset, profile, speed, joints, gauge"), more, [more]() {
EditorSettings.track_tools_more(false == more);
EditorSettings.save();
}, true});
if (false == more && false == advanced)
break;
add(track_tab::turntable, true);
add(track_tab::srk, false);
add(track_tab::kilometrage, false);
add(track_tab::lineside, false);
Buttons.push_back({icon::vehicle, STR_C("Trainset"), nullptr, STR_C("A trainset on the selected track: written to the scenery on save, and to drive at once"), vehicle, [this, vehicle]() {
show_track_tab(vehicle ? track_tab::path : track_tab::lineside);
m_vehicle.tab().expand = false == vehicle;
}});
Buttons.back().accent = family_colour(1);
add(track_tab::profile, true);
add(track_tab::speed, false);
add(track_tab::joints, 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.is_open(),
[this]() { m_gauge.toggle(); }});
Buttons.back().accent = family_colour(2);
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); }});
if (tool.tool == 2)
{
// what the place tool puts
char const *const kinds[] = {STR_C("Level crossing"), STR_C("Spawn point"), STR_C("Removal point"), STR_C("Pedestrian crossing")};
for (int index = 0; index < static_cast<int>(std::size(kinds)); ++index)
Buttons.push_back({icon::count, kinds[index], nullptr, nullptr, tool.placekind == index, [&tool, index]() { tool.placekind = index; }, index == 0});
}
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
auto const choose = [this](terrain_tool const Tool) { return [this, Tool]() { m_terrain_tool = Tool; }; };
auto const chosen = [this](terrain_tool const Tool) { return m_terrain_tool == Tool; };
Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), chosen(terrain_tool::none), choose(terrain_tool::none)});
Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), chosen(terrain_tool::sculpt), choose(terrain_tool::sculpt)});
Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), chosen(terrain_tool::smooth), choose(terrain_tool::smooth)});
Buttons.push_back({icon::profile, STR_C("Level"), nullptr, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"),
chosen(terrain_tool::level), choose(terrain_tool::level)});
Buttons.push_back({icon::brush, STR_C("Paint"), nullptr, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"), chosen(terrain_tool::paint), choose(terrain_tool::paint)});
Buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB removes it"), chosen(terrain_tool::chunks), choose(terrain_tool::chunks)});
Buttons.push_back({icon::area_fill, STR_C("Water"), nullptr, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"), chosen(terrain_tool::water), choose(terrain_tool::water)});
Buttons.push_back({icon::count, STR_C("Fill"), nullptr, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back"), chosen(terrain_tool::fill),
choose(terrain_tool::fill)});
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});
auto const advanced{m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::restore || m_orthophoto_window};
Buttons.push_back({icon::more, terrain_tools_more ? STR_C("Less") : STR_C("More"), nullptr, STR_C("Point spacing, restoring the terrain, orthophoto settings"), terrain_tools_more,
[]() { terrain_tools_more = false == terrain_tools_more; }, true});
if (false == terrain_tools_more && false == advanced)
break;
Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away; the tool options convert the point spacing of the selected"),
chosen(terrain_tool::spacing), choose(terrain_tool::spacing), true});
Buttons.push_back({icon::joints, STR_C("Restore terrain"), nullptr, STR_C("LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it"), chosen(terrain_tool::restore),
choose(terrain_tool::restore)});
Buttons.push_back({icon::count, STR_C("Orthophoto settings..."), nullptr, nullptr, m_orthophoto_window, [this]() { m_orthophoto_window = !m_orthophoto_window; }});
break;
}
}
}
void editor_mode::render_toolbar_caption(char const *Text)
{
// as wide in both rows, so the buttons of the rows start one under the other. the rows are drawn with no spacing of items
auto const margin{std::round(ImGui::GetFontSize() * 0.75f)};
auto const width{std::max(ImGui::CalcTextSize(STR_C("Work modes")).x, ImGui::CalcTextSize(STR_C("Tools")).x) + 2.0f * margin};
auto const position{ImGui::GetWindowPos()};
auto const height{ImGui::GetWindowHeight()};
auto *list{ImGui::GetWindowDrawList()};
auto const text{ImGui::CalcTextSize(Text)};
list->AddText(ImVec2(position.x + margin, std::round(position.y + (height - text.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_TextDisabled), Text);
list->AddLine(ImVec2(position.x + width, position.y + height * 0.2f), ImVec2(position.x + width, position.y + height * 0.8f), ImGui::GetColorU32(ImGuiCol_Separator));
ImGui::SetCursorScreenPos(position);
ImGui::Dummy(ImVec2(width + margin * 0.5f, height));
ImGui::SameLine(0.0f, 0.0f);
}
void editor_mode::render_toolbar_tools()
{
render_toolbar_caption(STR_C("Tools"));
std::vector<tool_button> buttons;
buttons.reserve(24);
work_area_tools(buttons);
if (buttons.empty())
return;
auto const s{scale()};
auto const iconside{editor_icons::size()};
auto const height{ImGui::GetWindowHeight()};
auto const margin{3.0f * s}; // over and under the buttons
auto const pad{7.0f * s}; // inside the button, at its ends
auto const spacing{5.0f * s}; // between the icon and the name
auto const gap{3.0f * s};
auto const groupgap{15.0f * s};
auto const buttonheight{height - 2.0f * margin};
auto const left{ImGui::GetCursorScreenPos().x};
auto const top{ImGui::GetWindowPos().y + margin};
// undo and redo at the right end of the row, the tools before them
auto const history_width{2.0f * buttonheight + gap};
auto const right{ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 4.0f * s - history_width - groupgap};
// the names next to the icons if there's room for them; if there isn't, the name of the chosen tool only, or none
enum class names
{
all,
chosen,
none
};
auto const named = [](tool_button const &Button, names const Names) {
return Button.picture == icon::count || Names == names::all || (Names == names::chosen && Button.chosen);
};
auto const width = [&](tool_button const &Button, names const Names) {
if (false == named(Button, Names))
return std::max(buttonheight, iconside + pad);
auto result{2.0f * pad + ImGui::CalcTextSize(Button.label).x};
if (Button.picture != icon::count)
result += iconside + spacing;
return result;
};
auto const total = [&](names const Names) {
auto result{0.0f};
for (std::size_t index = 0; index < buttons.size(); ++index)
result += width(buttons[index], Names) + (index == 0 ? 0.0f : buttons[index].group ? groupgap : gap);
return result;
};
auto shown{names::none};
for (auto const option : {names::all, names::chosen})
if (left + total(option) <= right)
{
shown = option;
break;
}
auto *list{ImGui::GetWindowDrawList()};
auto const rounding{3.0f * s};
auto x{left};
for (std::size_t index = 0; index < buttons.size(); ++index)
{
auto const &button{buttons[index]};
if (index > 0)
{
x += button.group ? groupgap : gap;
if (button.group)
list->AddLine(ImVec2(std::round(x - groupgap * 0.5f), top + buttonheight * 0.15f), ImVec2(std::round(x - groupgap * 0.5f), top + buttonheight * 0.85f), ImGui::GetColorU32(ImGuiCol_Separator));
}
auto const size{ImVec2(width(button, shown), buttonheight)};
ImVec2 const position{x, top};
ImGui::SetCursorScreenPos(position);
ImGui::PushID(static_cast<int>(index));
auto const clicked{ImGui::InvisibleButton("##tool", size)};
ImGui::PopID();
auto const hovered{ImGui::IsItemHovered()};
ImVec2 const end{position.x + size.x, position.y + size.y};
// the chosen one stands out as the chosen field of work does, the others as buttons
auto const background{button.chosen ? ImGui::GetColorU32(ImGuiCol_Header) : hovered && button.enabled ? ImGui::GetColorU32(ImGuiCol_HeaderHovered) : ImGui::GetColorU32(ImGuiCol_FrameBg)};
list->AddRectFilled(position, end, background, rounding);
if (button.chosen)
list->AddRectFilled(ImVec2(position.x, end.y - (button.accent != 0 ? 3.0f : 2.0f) * s), end, button.accent != 0 ? button.accent : ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, ImDrawFlags_RoundCornersBottom);
else if (button.accent != 0)
list->AddRectFilled(ImVec2(position.x + rounding, end.y - 1.0f * s), ImVec2(end.x - rounding, end.y), (button.accent & ~IM_COL32_A_MASK) | (IM_COL32(0, 0, 0, hovered ? 200 : 90) & IM_COL32_A_MASK));
auto const color{button.enabled ? ImGui::GetColorU32(ImGuiCol_Text, button.chosen || hovered ? 1.0f : 0.85f) : ImGui::GetColorU32(ImGuiCol_TextDisabled)};
auto const center{position.y + size.y * 0.5f};
if (named(button, shown))
{
auto textx{position.x + pad};
if (button.picture != icon::count)
{
editor_icons::draw(list, button.picture, ImVec2(textx + iconside * 0.5f, center), button.chosen && button.accent != 0 ? button.accent : color);
textx += iconside + spacing;
}
list->AddText(ImVec2(textx, std::round(center - ImGui::GetFontSize() * 0.5f)), color, button.label);
}
else
icon_or_letters(list, button, ImVec2(position.x + size.x * 0.5f, center), color);
if (hovered)
button_tooltip(button);
if (clicked && button.enabled && button.action)
button.action();
x = end.x;
}
// undo and redo, as Ctrl+Z and Ctrl+Y: not in the middle of a drag, it would go on from the state which was undone
auto const dragging{m_track_gizmo_using || m_track_set_using || m_straights.dragging || m_extend.active || m_switch.placing || m_point_drag.active || m_handle_drag.active};
auto const history_left{ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 4.0f * s - history_width};
for (int index = 0; index < 2; ++index)
{
auto const redo{index == 1};
auto const steps{redo ? g_redo.size() : m_history.size()};
auto const enabled{steps > 0 && false == dragging && false == m_history_busy};
ImVec2 const position{history_left + index * (buttonheight + gap), top};
ImVec2 const end{position.x + buttonheight, position.y + buttonheight};
ImGui::SetCursorScreenPos(position);
ImGui::PushID(redo ? "##redo" : "##undo");
auto const clicked{ImGui::InvisibleButton("##history", ImVec2(buttonheight, buttonheight))};
ImGui::PopID();
auto const hovered{ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)};
list->AddRectFilled(position, end, hovered && enabled ? ImGui::GetColorU32(ImGuiCol_HeaderHovered) : ImGui::GetColorU32(ImGuiCol_FrameBg), rounding);
auto const colour{enabled ? ImGui::GetColorU32(ImGuiCol_Text, hovered ? 1.0f : 0.85f) : ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.6f)};
// a hook of an arrow: back to the left for undo, on to the right for redo
auto const centre{ImVec2(position.x + buttonheight * 0.5f, position.y + buttonheight * 0.5f)};
auto const r{buttonheight * 0.22f};
auto const direction{redo ? -1.0f : 1.0f};
ImVec2 const from{centre.x + direction * r, centre.y + r * 0.9f};
ImVec2 const to{centre.x - direction * r * 0.2f, centre.y - r * 0.55f};
list->AddBezierCubic(from, ImVec2(centre.x + direction * r * 1.6f, centre.y + r * 0.2f), ImVec2(centre.x + direction * r * 1.2f, centre.y - r * 0.6f), to, colour, 1.8f * s);
list->AddLine(to, ImVec2(to.x - direction * r * 0.9f, to.y), colour, 1.8f * s);
list->AddTriangleFilled(ImVec2(to.x - direction * r * 1.45f, to.y), ImVec2(to.x - direction * r * 0.75f, to.y - r * 0.5f), ImVec2(to.x - direction * r * 0.75f, to.y + r * 0.5f), colour);
if (hovered)
ImGui::SetTooltip("%s", (redo ? format(STR_C("Redo (Ctrl+Y): %zu steps"), steps) : format(STR_C("Undo (Ctrl+Z): %zu steps"), steps)).c_str());
if (clicked && enabled)
{
if (redo)
redo_last();
else
undo_last();
}
}
}
void editor_mode::render_viewport_gizmo()
{
// the gizmo moves the models and the tracks; on the terrain the mouse is the brush's
// in the tracks the row stays with the gizmo hidden, so it can be brought out with a click
auto const tracks{ui()->mode() == nodebank_panel::TRACK};
if ((false == tracks && false == m_gizmo_enabled) || current_work_area() == work_area::terrain || (tracks && track_plan_tab()))
return;
auto const tooltip{STR_C("What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values")};
auto const on{gizmo_shown()};
auto const choose = [this](gizmo_operation const Operation) {
m_gizmo_op = Operation;
if (ui()->mode() == nodebank_panel::TRACK)
m_track_gizmo_on = true;
};
std::vector<tool_button> buttons;
if (tracks)
buttons.push_back({icon::select, STR_C("No gizmo"), nullptr, STR_C("The gizmo hidden: a track is changed by dragging its points, and won't move by chance"), false == on, [this]() { m_track_gizmo_on = false; }});
buttons.push_back({icon::translate, STR_C("Translate"), "Q", tooltip, on && m_gizmo_op == gizmo_operation::translate, [choose]() { choose(gizmo_operation::translate); }, tracks});
buttons.push_back({icon::rotate, STR_C("Rotate"), "W", tooltip, on && m_gizmo_op == gizmo_operation::rotate, [choose]() { choose(gizmo_operation::rotate); }});
buttons.push_back({icon::scale, STR_C("Scale"), "E", tooltip, on && m_gizmo_op == gizmo_operation::scale, [choose]() { choose(gizmo_operation::scale); }});
// the gizmo always scales along the axes of the model
buttons.push_back({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"), on && (m_gizmo_local || m_gizmo_op == gizmo_operation::scale),
[this]() { m_gizmo_local = !m_gizmo_local; }, true, on && 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()};
float width{0.0f};
for (auto const &button : buttons)
width += side + (&button == &buttons.front() ? 0.0f : button.group ? groupgap : gap);
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;
}