/* 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 #include #include #include #include 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 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 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 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(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 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(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 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; }