mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-11-07 06:28:08 +01: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:
@@ -185,6 +185,7 @@ set(SOURCES
|
|||||||
"editor/editorTerrainStreamer.cpp"
|
"editor/editorTerrainStreamer.cpp"
|
||||||
"editor/editorOrthophoto.cpp"
|
"editor/editorOrthophoto.cpp"
|
||||||
"editor/editorPreviews.cpp"
|
"editor/editorPreviews.cpp"
|
||||||
|
"editor/editorIcons.cpp"
|
||||||
"application/editormode.cpp"
|
"application/editormode.cpp"
|
||||||
"application/editormodetrack.cpp"
|
"application/editormodetrack.cpp"
|
||||||
"application/editormodetrackux.cpp"
|
"application/editormodetrackux.cpp"
|
||||||
@@ -198,6 +199,7 @@ set(SOURCES
|
|||||||
"application/editormodelineside.cpp"
|
"application/editormodelineside.cpp"
|
||||||
"application/editormodevehicle.cpp"
|
"application/editormodevehicle.cpp"
|
||||||
"application/editormodeselection.cpp"
|
"application/editormodeselection.cpp"
|
||||||
|
"application/editormodeviewport.cpp"
|
||||||
"application/editormodesignal.cpp"
|
"application/editormodesignal.cpp"
|
||||||
"application/editormodesweep.cpp"
|
"application/editormodesweep.cpp"
|
||||||
"application/editormodeturntable.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")
|
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 (PREFIX "")
|
||||||
set (SUFFIX "")
|
set (SUFFIX "")
|
||||||
|
|
||||||
|
|||||||
@@ -1344,6 +1344,8 @@ bool editor_mode::update()
|
|||||||
|
|
||||||
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
|
// --- ImGuizmo: in-viewport transform gizmo for the selected node ---
|
||||||
render_gizmo();
|
render_gizmo();
|
||||||
|
// the tools, the gizmo modes and the axes over the 3d view
|
||||||
|
render_viewport_overlays();
|
||||||
|
|
||||||
render_track_inspector();
|
render_track_inspector();
|
||||||
if (selected_track())
|
if (selected_track())
|
||||||
@@ -3897,29 +3899,7 @@ void editor_mode::render_toolbar()
|
|||||||
case work_area::roads: render_road_toolbar(); break;
|
case work_area::roads: render_road_toolbar(); break;
|
||||||
case work_area::terrain: render_terrain_toolbar(); break;
|
case work_area::terrain: render_terrain_toolbar(); break;
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
// the modes of the gizmo and the top view are over the 3d view, see render_viewport_overlays()
|
||||||
// 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<char const *, gizmo_operation> 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"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void editor_mode::render_edit_menu()
|
void editor_mode::render_edit_menu()
|
||||||
|
|||||||
@@ -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
|
// time of day, day of the year, visibility, clouds and air temperature, as the scenery is seen in the editor
|
||||||
void render_environment_window();
|
void render_environment_window();
|
||||||
bool m_environment_open{false};
|
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
|
// 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_read{false};
|
||||||
bool m_georeference{false};
|
bool m_georeference{false};
|
||||||
|
|||||||
398
application/editormodeviewport.cpp
Normal file
398
application/editormodeviewport.cpp
Normal 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;
|
||||||
|
}
|
||||||
176
editor/editorIcons.cpp
Normal file
176
editor/editorIcons.cpp
Normal file
@@ -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 <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<int>(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<std::uint8_t> const &coverage()
|
||||||
|
{
|
||||||
|
static std::vector<std::uint8_t> const pixels = []() {
|
||||||
|
std::vector<std::uint8_t> 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<int>(sizeof(png)), &width, &height, &components, 4)};
|
||||||
|
if (rgba == nullptr)
|
||||||
|
return;
|
||||||
|
if (width == columns * cell && height >= rows * cell)
|
||||||
|
{
|
||||||
|
result.resize(static_cast<std::size_t>(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<float>(Pixel))};
|
||||||
|
auto const end{std::min((Target + 1) * Scale, static_cast<float>(Pixel + 1))};
|
||||||
|
return std::max(0.0f, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> scaled(int const Size)
|
||||||
|
{
|
||||||
|
auto const &source{coverage()};
|
||||||
|
auto const width{columns * stride(Size)};
|
||||||
|
auto const height{rows * stride(Size)};
|
||||||
|
std::vector<std::uint8_t> rgba(static_cast<std::size_t>(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<float>(cell) / Size};
|
||||||
|
std::vector<float> across(static_cast<std::size_t>(Size) * cell);
|
||||||
|
for (int index = 0; index < static_cast<int>(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<int>(x * scale); pixel < std::min(cell, static_cast<int>(std::ceil((x + 1) * scale))); ++pixel)
|
||||||
|
sum += overlap(x, scale, pixel) * source[static_cast<std::size_t>(sourcey + y) * columns * cell + sourcex + pixel];
|
||||||
|
across[static_cast<std::size_t>(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<int>(y * scale); pixel < std::min(cell, static_cast<int>(std::ceil((y + 1) * scale))); ++pixel)
|
||||||
|
sum += overlap(y, scale, pixel) * across[static_cast<std::size_t>(pixel) * Size + x];
|
||||||
|
rgba[(static_cast<std::size_t>(targety + y) * width + targetx + x) * 4 + 3] = static_cast<std::uint8_t>(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<int>(size())};
|
||||||
|
if (List == nullptr || Icon >= icon::count || false == prepare(pixels))
|
||||||
|
return false;
|
||||||
|
auto const index{static_cast<int>(Icon)};
|
||||||
|
auto const width{static_cast<float>(columns * stride(pixels))};
|
||||||
|
auto const height{static_cast<float>(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<ImTextureID>(g_atlas.texture), min, ImVec2(min.x + pixels, min.y + pixels), uv0, uv1, Color);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace editor_icons
|
||||||
61
editor/editorIcons.hpp
Normal file
61
editor/editorIcons.hpp
Normal file
@@ -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
|
||||||
BIN
editor/icons/editor_icons.png
Normal file
BIN
editor/icons/editor_icons.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
364
editor/icons/editor_icons.py
Normal file
364
editor/icons/editor_icons.py
Normal file
@@ -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()
|
||||||
@@ -5024,3 +5024,32 @@ msgstr "Widoczne tylko w edytorze; sceneria zachowuje własny czas i pogodę"
|
|||||||
|
|
||||||
msgid "Top view, orthographic (O)"
|
msgid "Top view, orthographic (O)"
|
||||||
msgstr "Widok z góry, ortogonalny (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"
|
||||||
|
|||||||
Reference in New Issue
Block a user