diff --git a/CMakeLists.txt b/CMakeLists.txt index 300427ac..332dd832 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,6 +185,7 @@ set(SOURCES "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "editor/editorPreviews.cpp" +"editor/editorIcons.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormodetrackux.cpp" @@ -198,6 +199,7 @@ set(SOURCES "application/editormodelineside.cpp" "application/editormodevehicle.cpp" "application/editormodeselection.cpp" +"application/editormodeviewport.cpp" "application/editormodesignal.cpp" "application/editormodesweep.cpp" "application/editormodeturntable.cpp" @@ -381,6 +383,14 @@ endif() configure_file("${CMAKE_SOURCE_DIR}/version_info.h.in" "${CMAKE_BINARY_DIR}/geninc/version_info.h") +# the icons of the editor are built into the executable: the png is written out as the bytes of an array, see editor/editorIcons.cpp +set(EDITOR_ICONS "${CMAKE_SOURCE_DIR}/editor/icons/editor_icons.png") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${EDITOR_ICONS}") +file(READ "${EDITOR_ICONS}" EDITOR_ICONS_HEX HEX) +string(REGEX REPLACE "(................................)" "\\1\n" EDITOR_ICONS_HEX "${EDITOR_ICONS_HEX}") +string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," EDITOR_ICONS_HEX "${EDITOR_ICONS_HEX}") +file(CONFIGURE OUTPUT "${CMAKE_BINARY_DIR}/geninc/editor_icons_png.inc" CONTENT "${EDITOR_ICONS_HEX}\n") + set (PREFIX "") set (SUFFIX "") diff --git a/application/editormode.cpp b/application/editormode.cpp index 5f99a52a..0d2b6acc 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1344,6 +1344,8 @@ bool editor_mode::update() // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); + // the tools, the gizmo modes and the axes over the 3d view + render_viewport_overlays(); render_track_inspector(); if (selected_track()) @@ -3897,29 +3899,7 @@ void editor_mode::render_toolbar() case work_area::roads: render_road_toolbar(); break; case work_area::terrain: render_terrain_toolbar(); break; } - ImGui::Separator(); - // the gizmo moves the models and the tracks; on the terrain the mouse is the brush's - if (m_gizmo_enabled && area != work_area::terrain) - { - std::pair const operations[] = { - {STR_C("Translate (Q)"), gizmo_operation::translate}, {STR_C("Rotate (W)"), gizmo_operation::rotate}, {STR_C("Scale (E)"), gizmo_operation::scale}}; - for (auto const &operation : operations) - { - 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")); + // the modes of the gizmo and the top view are over the 3d view, see render_viewport_overlays() } void editor_mode::render_edit_menu() diff --git a/application/editormode.h b/application/editormode.h index 25ee8451..edc45daa 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -385,6 +385,14 @@ class editor_mode : public application_mode, private editor_track::observer // time of day, day of the year, visibility, clouds and air temperature, as the scenery is seen in the editor void render_environment_window(); bool m_environment_open{false}; + // 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 + void render_viewport_overlays(); + void render_viewport_tools(); + void render_viewport_gizmo(); + void render_view_axes(); + // the camera looking along an axis of the world (0: x, 1: y, 2: z) from its positive or negative side + void look_along_axis(int const Axis, bool const Positive); // origin of the scenery in PUWG 1992 the scenery file gives in its //$g line, if it does bool m_georeference_read{false}; bool m_georeference{false}; diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp new file mode 100644 index 00000000..17434652 --- /dev/null +++ b/application/editormodeviewport.cpp @@ -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 +#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; +} diff --git a/editor/editorIcons.cpp b/editor/editorIcons.cpp new file mode 100644 index 00000000..6056763f --- /dev/null +++ b/editor/editorIcons.cpp @@ -0,0 +1,176 @@ +/* +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/. +*/ + +#include "stdafx.h" +#include "editor/editorIcons.hpp" + +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "stb/stb_image.h" + +#include +#include +#include +#include +#include + +namespace editor_icons +{ + +namespace +{ + +// editor_icons.png, written out by CMake +unsigned char const png[] = { +#include "editor_icons_png.inc" +}; + +int const cell{64}; // pixels of an icon in the png +int const columns{8}; +int const rows{(static_cast(icon::count) + columns - 1) / columns}; + +// the coverage of the icons, a byte a pixel, as they're in the png. empty if it couldn't be read +std::vector const &coverage() +{ + static std::vector const pixels = []() { + std::vector result; + // read in a thread of its own: the texture loader turns the images upside down for the threads which don't say otherwise, + // and what a thread says stays with it + std::thread reader([&result]() { + stbi_set_flip_vertically_on_load_thread(0); + int width{0}, height{0}, components{0}; + auto *rgba{stbi_load_from_memory(png, static_cast(sizeof(png)), &width, &height, &components, 4)}; + if (rgba == nullptr) + return; + if (width == columns * cell && height >= rows * cell) + { + result.resize(static_cast(width) * height); + for (std::size_t index = 0; index < result.size(); ++index) + result[index] = rgba[index * 4 + 3]; + } + stbi_image_free(rgba); + }); + reader.join(); + return result; + }(); + return pixels; +} + +// the icons scaled down to the size they're drawn at, each with a pixel of empty space around it, so that the filtering +// doesn't pick up a neighbour +struct atlas +{ + std::uint64_t texture{0}; + int size{0}; // pixels of an icon + bool failed{false}; +}; +atlas g_atlas; + +int stride(int const Size) +{ + return Size + 2; +} + +// part of the source pixel Pixel the target pixel Target of the size Scale covers +float overlap(int const Target, float const Scale, int const Pixel) +{ + auto const start{std::max(Target * Scale, static_cast(Pixel))}; + auto const end{std::min((Target + 1) * Scale, static_cast(Pixel + 1))}; + return std::max(0.0f, end - start); +} + +std::vector scaled(int const Size) +{ + auto const &source{coverage()}; + auto const width{columns * stride(Size)}; + auto const height{rows * stride(Size)}; + std::vector rgba(static_cast(width) * height * 4, 255); + for (std::size_t index = 3; index < rgba.size(); index += 4) + rgba[index] = 0; + // each target pixel the average of the source pixels under it, the ones under its edges by the part they're under it + auto const scale{static_cast(cell) / Size}; + std::vector across(static_cast(Size) * cell); + for (int index = 0; index < static_cast(icon::count); ++index) + { + auto const sourcex{(index % columns) * cell}; + auto const sourcey{(index / columns) * cell}; + for (int y = 0; y < cell; ++y) + { + for (int x = 0; x < Size; ++x) + { + float sum{0.0f}; + for (int pixel = static_cast(x * scale); pixel < std::min(cell, static_cast(std::ceil((x + 1) * scale))); ++pixel) + sum += overlap(x, scale, pixel) * source[static_cast(sourcey + y) * columns * cell + sourcex + pixel]; + across[static_cast(y) * Size + x] = sum / scale; + } + } + auto const targetx{(index % columns) * stride(Size) + 1}; + auto const targety{(index / columns) * stride(Size) + 1}; + for (int y = 0; y < Size; ++y) + { + for (int x = 0; x < Size; ++x) + { + float sum{0.0f}; + for (int pixel = static_cast(y * scale); pixel < std::min(cell, static_cast(std::ceil((y + 1) * scale))); ++pixel) + sum += overlap(y, scale, pixel) * across[static_cast(pixel) * Size + x]; + rgba[(static_cast(targety + y) * width + targetx + x) * 4 + 3] = static_cast(std::clamp(sum / scale + 0.5f, 0.0f, 255.0f)); + } + } + } + return rgba; +} + +// the texture of the icons at the size, made again when the size changes +bool prepare(int const Size) +{ + if (g_atlas.failed) + return false; + if (g_atlas.texture != 0 && g_atlas.size == Size) + return true; + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + if (renderer == nullptr || coverage().empty()) + { + g_atlas.failed = true; + return false; + } + if (g_atlas.texture != 0) + renderer->Release_Image(g_atlas.texture); + auto const rgba{scaled(Size)}; + g_atlas.texture = renderer->Create_Image(rgba.data(), columns * stride(Size), rows * stride(Size)); + g_atlas.size = Size; + // a renderer which can't show them gets no more requests + g_atlas.failed = (g_atlas.texture == 0); + return false == g_atlas.failed; +} + +} // namespace + +float size() +{ + return std::round(20.0f * std::max(1.0f, Global.ui_scale)); +} + +bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color) +{ + // all at the one size, the texture is made for it + auto const pixels{static_cast(size())}; + if (List == nullptr || Icon >= icon::count || false == prepare(pixels)) + return false; + auto const index{static_cast(Icon)}; + auto const width{static_cast(columns * stride(pixels))}; + auto const height{static_cast(rows * stride(pixels))}; + ImVec2 const uv0{((index % columns) * stride(pixels) + 1) / width, ((index / columns) * stride(pixels) + 1) / height}; + ImVec2 const uv1{uv0.x + pixels / width, uv0.y + pixels / height}; + // on whole pixels, the icon was scaled for them + ImVec2 const min{std::round(Center.x - pixels * 0.5f), std::round(Center.y - pixels * 0.5f)}; + List->AddImage(static_cast(g_atlas.texture), min, ImVec2(min.x + pixels, min.y + pixels), uv0, uv1, Color); + return true; +} + +} // namespace editor_icons diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp new file mode 100644 index 00000000..3734d465 --- /dev/null +++ b/editor/editorIcons.hpp @@ -0,0 +1,61 @@ +/* +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/. +*/ + +#pragma once + +#include "imgui/imgui.h" + +// icons of the scenery editor. they're drawn by editor/icons/editor_icons.py into editor_icons.png, which CMake writes out +// as the bytes of an array built into the executable, so they're there without any file of the simulator +namespace editor_icons +{ + +// in the order of the cells of editor_icons.png +enum class icon +{ + select, + insert, + brush, + area_fill, + copy_to_bank, + translate, + rotate, + scale, + local_space, + ortho, + perspective, + sculpt, + smooth, + chunks, + orthophoto, + road, + place, + lanes, + lay_track, + turnout, + straight, + curve, + signal, + objects, + vehicle, + profile, + speed, + joints, + infra, + gauge, + turntable, + count +}; + +// pixels of the side of an icon, at the scale of the user interface +float size(); +// draws the icon in the colour given, centred on the point. returns: false if the renderer can't show images +bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color); + +} // namespace editor_icons diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png new file mode 100644 index 00000000..80b7013b Binary files /dev/null and b/editor/icons/editor_icons.png differ diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py new file mode 100644 index 00000000..9b035485 --- /dev/null +++ b/editor/icons/editor_icons.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +# draws the icons of the scenery editor into editor_icons.png, next to this script: white shapes on a transparent ground, +# 64 x 64 pixels each, 8 in a row, in the order of editor_icons::icon (editor/editorIcons.hpp). +# the editor tints them as it draws them; the png is built into the executable by CMake, see CMakeLists.txt. +# needs Pillow: python3 editor_icons.py +import math +import os + +from PIL import Image, ImageDraw + +CELL = 64 +SUPER = 4 # drawn this many times bigger, then scaled down for smooth edges +S = CELL * SUPER +W = 18 # width of the lines, at the bigger size +COLUMNS = 8 + + +def p(x, y): + # coordinates given on a 64 x 64 grid + return (x * SUPER, y * SUPER) + + +def line(d, points, width=W): + pts = [p(*q) for q in points] + d.line(pts, fill=255, width=width, joint="curve") + for q in (pts[0], pts[-1]): + r = width / 2 + d.ellipse((q[0] - r, q[1] - r, q[0] + r, q[1] + r), fill=255) + + +def poly(d, points, outline=False, width=W): + pts = [p(*q) for q in points] + if outline: + line(d, points + [points[0]], width) + else: + d.polygon(pts, fill=255) + + +def circle(d, x, y, r, outline=False, width=W): + box = (p(x - r, y - r), p(x + r, y + r)) + if outline: + d.ellipse(box, outline=255, width=width) + else: + d.ellipse(box, fill=255) + + +def rect(d, x0, y0, x1, y1, outline=False, width=W, radius=0): + box = (p(x0, y0), p(x1, y1)) + if outline: + d.rounded_rectangle(box, radius=radius * SUPER, outline=255, width=width) + else: + d.rounded_rectangle(box, radius=radius * SUPER, fill=255) + + +def arrowhead(d, x, y, angle, size=9): + # filled head pointing at angle (degrees, 0 = right, 90 = down) + a = math.radians(angle) + tip = (x + math.cos(a) * size * 0.6, y + math.sin(a) * size * 0.6) + left = (x + math.cos(a + 2.5) * size, y + math.sin(a + 2.5) * size) + right = (x + math.cos(a - 2.5) * size, y + math.sin(a - 2.5) * size) + poly(d, [tip, left, right]) + + +def arc(d, x, y, r, start, end, width=W): + box = (p(x - r, y - r), p(x + r, y + r)) + d.arc(box, start, end, fill=255, width=width) + + +def dashed(d, a, b, dash=6, gap=4, width=W): + length = math.hypot(b[0] - a[0], b[1] - a[1]) + if length == 0: + return + ux, uy = (b[0] - a[0]) / length, (b[1] - a[1]) / length + t = 0.0 + while t < length: + e = min(t + dash, length) + line(d, [(a[0] + ux * t, a[1] + uy * t), (a[0] + ux * e, a[1] + uy * e)], width) + t = e + gap + + +# the icons, in the order of editor_icons::icon + + +def select(d): + poly(d, [(16, 8), (16, 50), (26, 40), (33, 56), (40, 53), (33, 37), (47, 37)]) + + +def insert(d): + # a box, with a plus over its corner + poly(d, [(8, 26), (28, 16), (48, 26), (28, 36)], outline=True) + line(d, [(8, 26), (8, 46), (28, 56), (48, 46), (48, 26)]) + line(d, [(28, 36), (28, 56)]) + circle(d, 48, 14, 11) + + +def brush(d): + line(d, [(52, 8), (30, 32)], W + 4) + poly(d, [(26, 30), (34, 38), (24, 52), (8, 56), (12, 42)]) + for x, y in ((44, 40), (52, 30), (50, 50)): + circle(d, x, y, 3) + + +def area_fill(d): + outline = [(8, 18), (34, 8), (56, 24), (50, 54), (16, 52)] + for i in range(len(outline)): + dashed(d, outline[i], outline[(i + 1) % len(outline)], 6, 5, W - 4) + for x, y in ((24, 38), (38, 30), (36, 44)): + poly(d, [(x, y - 9), (x + 6, y + 4), (x - 6, y + 4)]) + + +def copy_to_bank(d): + rect(d, 18, 6, 46, 26, outline=True, radius=3) + line(d, [(32, 26), (32, 40)]) + arrowhead(d, 32, 42, 90, 12) + line(d, [(8, 40), (8, 56), (56, 56), (56, 40)]) + + +def translate(d): + line(d, [(32, 10), (32, 54)]) + line(d, [(10, 32), (54, 32)]) + arrowhead(d, 32, 8, 270, 13) + arrowhead(d, 32, 56, 90, 13) + arrowhead(d, 8, 32, 180, 13) + arrowhead(d, 56, 32, 0, 13) + + +def rotate(d): + # turning clockwise, the gap at the top + arc(d, 32, 34, 21, 300, 225) + a = math.radians(225) + arrowhead(d, 32 + math.cos(a) * 21, 34 + math.sin(a) * 21, 315, 15) + circle(d, 32, 34, 4) + + +def scale(d): + rect(d, 8, 30, 34, 56, radius=2) + for a, b in (((8, 8), (56, 8)), ((56, 8), (56, 56)), ((8, 8), (8, 22)), ((42, 56), (56, 56))): + dashed(d, a, b, 6, 5, W - 6) + line(d, [(30, 34), (48, 16)]) + arrowhead(d, 50, 14, 315, 14) + + +def local_space(d): + # the axes turned with the model + o = (22, 42) + for angle, length in ((-20, 34), (-110, 30), (150, 18)): + a = math.radians(angle) + e = (o[0] + math.cos(a) * length, o[1] + math.sin(a) * length) + line(d, [o, e], W - 2) + arrowhead(d, e[0], e[1], angle, 11) + circle(d, o[0], o[1], 5) + + +def ortho(d): + # the plan seen from over it: a grid without perspective + rect(d, 8, 8, 56, 56, outline=True, width=W - 2) + for t in (24, 40): + line(d, [(t, 8), (t, 56)], W - 8) + line(d, [(8, t), (56, t)], W - 8) + + +def perspective(d): + # the grid going away + poly(d, [(20, 14), (44, 14), (58, 54), (6, 54)], outline=True, width=W - 2) + line(d, [(28, 14), (24, 54)], W - 8) + line(d, [(36, 14), (40, 54)], W - 8) + line(d, [(15, 30), (49, 30)], W - 8) + + +def sculpt(d): + poly(d, [(4, 56), (22, 26), (32, 38), (42, 22), (60, 56)]) + line(d, [(50, 18), (50, 4)], W - 4) + arrowhead(d, 50, 3, 270, 11) + + +def smooth(d): + pts = [(x, 40 - 12 * math.sin((x - 6) / 52 * math.pi)) for x in range(6, 59, 2)] + line(d, pts) + line(d, [(6, 54), (58, 54)], W - 6) + circle(d, 32, 28, 14, outline=True, width=W - 8) + + +def chunks(d): + for x, y in ((6, 6), (33, 6), (6, 33)): + rect(d, x, y, x + 25, y + 25, radius=3) + rect(d, 33, 33, 58, 58, outline=True, width=W - 6, radius=3) + line(d, [(45.5, 39), (45.5, 52)], W - 8) + line(d, [(39, 45.5), (52, 45.5)], W - 8) + + +def orthophoto(d): + rect(d, 6, 10, 58, 54, outline=True, radius=4) + poly(d, [(10, 50), (26, 28), (36, 40), (42, 34), (54, 50)]) + circle(d, 44, 21, 5) + + +def road(d): + line(d, [(24, 8), (8, 56)]) + line(d, [(40, 8), (56, 56)]) + dashed(d, (32, 8), (32, 56), 8, 6, W - 4) + + +def place(d): + # a pin standing on the place + poly(d, [(32, 58), (18, 34), (46, 34)]) + circle(d, 32, 26, 15) + + +def lanes(d): + for x in (14, 50): + line(d, [(x, 6), (x, 58)], W - 4) + dashed(d, (32, 6), (32, 58), 7, 6, W - 6) + line(d, [(23, 52), (23, 22)], W - 6) + arrowhead(d, 23, 18, 270, 10) + line(d, [(41, 12), (41, 42)], W - 6) + arrowhead(d, 41, 46, 90, 10) + + +def lay_track(d): + # two rails on sleepers, going up to the right + for t in range(0, 6): + x, y = 10 + t * 9, 54 - t * 9 + line(d, [(x - 8, y - 8), (x + 8, y + 8)], W - 8) + line(d, [(4, 50), (50, 4)], W - 4) + line(d, [(14, 60), (60, 14)], W - 4) + + +def switch(d): + line(d, [(18, 60), (18, 4)], W - 2) + line(d, [(34, 60), (34, 4)], W - 2) + pts = [(18 + 30 * (1 - math.cos(t / 20 * math.pi / 2)), 52 - t * 2.4) for t in range(0, 21)] + line(d, pts, W - 2) + pts = [(34 + 22 * (1 - math.cos(t / 20 * math.pi / 2)), 60 - t * 2.4) for t in range(0, 21)] + line(d, pts, W - 2) + + +def straight(d): + line(d, [(12, 52), (52, 12)]) + rect(d, 4, 44, 20, 60, radius=2) + rect(d, 44, 4, 60, 20, radius=2) + + +def curve(d): + arc(d, 56, 56, 44, 180, 270) + dashed(d, (56, 56), (25, 25), 5, 4, W - 8) + circle(d, 12, 56, 6) + circle(d, 56, 12, 6) + circle(d, 25, 25, 6) + + +def signal(d): + line(d, [(32, 34), (32, 60)], W - 2) + line(d, [(20, 60), (44, 60)], W - 2) + rect(d, 20, 4, 44, 38, radius=10) + + +def objects(d): + # a post by the track with a board on it + line(d, [(24, 30), (24, 60)], W - 2) + rect(d, 10, 6, 38, 30, radius=3) + line(d, [(42, 60), (60, 60)], W - 6) + line(d, [(48, 52), (60, 52)], W - 6) + + +def vehicle(d): + poly(d, [(4, 22), (50, 22), (60, 32), (60, 46), (4, 46)]) + circle(d, 16, 52, 6) + circle(d, 46, 52, 6) + line(d, [(4, 58), (60, 58)], W - 8) + + +def profile(d): + pts = [(6, 46), (22, 30), (38, 36), (58, 14)] + line(d, pts) + for q in pts: + circle(d, q[0], q[1], 5) + line(d, [(6, 58), (58, 58)], W - 8) + + +def speed(d): + arc(d, 32, 40, 24, 180, 360) + for a in (200, 240, 280, 320): + r = math.radians(a) + line(d, [(32 + math.cos(r) * 15, 40 + math.sin(r) * 15), (32 + math.cos(r) * 20, 40 + math.sin(r) * 20)], W - 8) + line(d, [(32, 40), (46, 24)], W - 2) + circle(d, 32, 40, 6) + + +def joints(d): + line(d, [(4, 26), (26, 26)]) + line(d, [(38, 26), (60, 26)]) + line(d, [(4, 40), (26, 40)]) + line(d, [(38, 44), (60, 44)]) + rect(d, 28, 14, 36, 54, outline=True, width=W - 10, radius=2) + + +def infra(d): + # a catenary mast with its arm and the wire hanging from it + line(d, [(14, 60), (14, 6)], W + 2) + line(d, [(14, 12), (54, 12)], W - 4) + line(d, [(14, 26), (46, 12)], W - 8) + line(d, [(44, 12), (44, 28)], W - 8) + pts = [(x, 30 + 0.012 * (x - 36) ** 2) for x in range(14, 61, 2)] + line(d, pts, W - 6) + + +def gauge(d): + # the outline of the structure gauge over the rails + line(d, [(12, 52), (12, 22), (22, 8), (42, 8), (52, 22), (52, 52)], W - 4) + line(d, [(4, 56), (60, 56)], W - 6) + for x in (24, 40): + rect(d, x - 3, 48, x + 3, 56) + + +def turntable(d): + # the pit with its bridge, and the tracks leading out of it + circle(d, 32, 32, 20, outline=True, width=W - 4) + line(d, [(18, 46), (46, 18)], W) + for a in (-90, -30, 30): + r = math.radians(a) + line(d, [(32 + math.cos(r) * 24, 32 + math.sin(r) * 24), (32 + math.cos(r) * 31, 32 + math.sin(r) * 31)], W - 4) + + +ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, + local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, + place, lanes, lay_track, switch, straight, curve, signal, objects, + vehicle, profile, speed, joints, infra, gauge, turntable] + + +def cutouts(name, d): + # the parts drawn in the colour of the ground: the plus of the insert, the hole of the pin, the lights of the signal + if name == "insert": + line(d, [(48, 8), (48, 20)], W - 8) + line(d, [(42, 14), (54, 14)], W - 8) + elif name == "place": + circle(d, 32, 26, 6) + elif name == "signal": + circle(d, 32, 14, 5) + circle(d, 32, 28, 5) + elif name == "vehicle": + rect(d, 10, 27, 20, 35) + rect(d, 26, 27, 36, 35) + rect(d, 42, 27, 50, 35) + + +def main(): + rows = (len(ICONS) + COLUMNS - 1) // COLUMNS + atlas = Image.new("RGBA", (COLUMNS * CELL, rows * CELL), (255, 255, 255, 0)) + for index, draw in enumerate(ICONS): + mask = Image.new("L", (S, S), 0) + d = ImageDraw.Draw(mask) + draw(d) + holes = Image.new("L", (S, S), 0) + cutouts(draw.__name__, ImageDraw.Draw(holes)) + mask.paste(0, (0, 0), holes) + mask = mask.resize((CELL, CELL), Image.LANCZOS) + icon = Image.new("RGBA", (CELL, CELL), (255, 255, 255, 0)) + icon.putalpha(mask) + atlas.paste(icon, ((index % COLUMNS) * CELL, (index // COLUMNS) * CELL)) + atlas.save(os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor_icons.png"), optimize=True) + + +if __name__ == "__main__": + main() diff --git a/lang/editor_pl.po b/lang/editor_pl.po index e8b593be..1c3dd20c 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5024,3 +5024,32 @@ msgstr "Widoczne tylko w edytorze; sceneria zachowuje własny czas i pogodę" msgid "Top view, orthographic (O)" msgstr "Widok z góry, ortogonalny (O)" + +# scenery editor: tools over the 3d view + +msgid "Translate" +msgstr "Przesuwanie" + +msgid "Rotate" +msgstr "Obrót" + +msgid "Scale" +msgstr "Skala" + +msgid "Look along %s%s; from over the scenery it's the top view" +msgstr "Spójrz wzdłuż %s%s; znad scenerii to widok z góry" + +msgid "Show lanes" +msgstr "Pokaż pasy ruchu" + +msgid "The lanes of the roads drawn as the paths the vehicles take" +msgstr "Pasy ruchu dróg rysowane jako tory jazdy pojazdów" + +msgid "LMB: a road piece, a junction, a point where pieces meet, a level crossing or a traffic point" +msgstr "LPM: kawałek drogi, skrzyżowanie, punkt łączenia kawałków, przejazd kolejowy albo punkt ruchu" + +msgid "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" +msgstr "LPM: początek drogi, potem każdy kolejny punkt; na wolnym końcu, boku drogi albo skrzyżowaniu droga zostaje do nich dołączona" + +msgid "Level crossings, and the points where vehicles appear on the roads or are taken off them" +msgstr "Przejazdy kolejowe oraz punkty, w których pojazdy pojawiają się na drogach albo są z nich zdejmowane"