17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-04 19:59:25 +01:00

feat(tracks): track editor in one movable Tracks window with exclusive modes, a guide of what can be done in the current step and the rarely used fields folded; vertical profile window with the ground of the scenery triangles; plain pieces added at a free end, freehand only on request; curve mode edits the single curve clicked

This commit is contained in:
maj00r
2026-10-05 01:21:11 +02:00
parent 220fd0f37e
commit 3731520e33
13 changed files with 2769 additions and 924 deletions

View File

@@ -175,6 +175,7 @@ set(SOURCES
"editor/editorTrack.cpp" "editor/editorTrack.cpp"
"editor/editorRoad.cpp" "editor/editorRoad.cpp"
"editor/editorAlignment.cpp" "editor/editorAlignment.cpp"
"editor/editorSpeed.cpp"
"editor/editorProfile.cpp" "editor/editorProfile.cpp"
"editor/editorGauge.cpp" "editor/editorGauge.cpp"
"editor/editorInfra.cpp" "editor/editorInfra.cpp"
@@ -186,6 +187,7 @@ set(SOURCES
"application/editormodeprofile.cpp" "application/editormodeprofile.cpp"
"application/editormodegauge.cpp" "application/editormodegauge.cpp"
"application/editormodeinfra.cpp" "application/editormodeinfra.cpp"
"application/editormodespeed.cpp"
"input/editormouseinput.cpp" "input/editormouseinput.cpp"
"application/editoruilayer.cpp" "application/editoruilayer.cpp"
"application/editoruipanels.cpp" "application/editoruipanels.cpp"

View File

@@ -475,10 +475,9 @@ editor_mode::editor_mode() {
ui()->set_fill_options([this]() { render_area_fill(); }); ui()->set_fill_options([this]() { render_area_fill(); });
ui()->set_gizmo_options([this]() { render_gizmo_options(); }); ui()->set_gizmo_options([this]() { render_gizmo_options(); });
ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); });
ui()->set_track_options([this]() { render_track_ui(); });
ui()->set_gauge_window(&m_gauge.open);
ui()->set_menu_options([this]() { ui()->set_menu_options([this]() {
render_object_menu(); render_object_menu();
render_track_menu();
render_road_menu(); render_road_menu();
}); });
m_roadtool.settings.normalize(); m_roadtool.settings.normalize();
@@ -496,10 +495,12 @@ editor_mode::~editor_mode()
editor_track::observe(nullptr); editor_track::observe(nullptr);
} }
std::vector<double> editor_mode::ground_heights(std::vector<glm::dvec3> const &Points, bool const Fresh) std::vector<double> editor_mode::ground_heights(std::vector<glm::dvec3> const &Points, bool const Fresh, std::vector<char> *Found)
{ {
std::vector<double> heights; std::vector<double> heights;
heights.reserve(Points.size()); heights.reserve(Points.size());
if (Found != nullptr)
Found->assign(Points.size(), 0);
for (auto const &point : Points) for (auto const &point : Points)
heights.push_back(point.y); heights.push_back(point.y);
if (Points.empty() || simulation::Region == nullptr) if (Points.empty() || simulation::Region == nullptr)
@@ -548,6 +549,8 @@ std::vector<double> editor_mode::ground_heights(std::vector<glm::dvec3> const &P
} }
if (found) if (found)
heights[i] = y; heights[i] = y;
if (Found != nullptr)
(*Found)[i] = found ? 1 : 0;
} }
// the tiles left alone the longest go once there are too many // the tiles left alone the longest go once there are too many
while (ground_tiles.size() > ground_tile_limit) while (ground_tiles.size() > ground_tile_limit)
@@ -1241,6 +1244,7 @@ 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();
render_track_inspector();
if (selected_track()) if (selected_track())
draw_track_overlay(); draw_track_overlay();
if (route_active()) if (route_active())
@@ -1251,12 +1255,11 @@ bool editor_mode::update()
{ {
update_build_tools(); update_build_tools();
draw_build_overlay(); draw_build_overlay();
speed_step();
draw_track_hints(); draw_track_hints();
render_track_window();
draw_profile_overlay(); draw_profile_overlay();
render_profile_window();
draw_infra_overlay(); draw_infra_overlay();
render_infra_window(); draw_speed_overlay();
} }
update_gauge(); update_gauge();
render_gauge_window(); render_gauge_window();
@@ -2647,7 +2650,11 @@ void editor_mode::render_gizmo()
if (selected_track()) if (selected_track())
{ {
render_track_gizmo(); // in the track modes the whole path is moved only while selecting, or the switch in its mode
if (ui()->mode() != nodebank_panel::TRACK || m_track_tab == track_tab::path || m_track_tab == track_tab::turnout)
render_track_gizmo();
else
m_track_gizmo_using = false;
return; return;
} }
@@ -3010,6 +3017,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
if (!anyModifier && is_press(Action) if (!anyModifier && is_press(Action)
&& m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) != GLFW_PRESS) && m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) != GLFW_PRESS)
{ {
if (ui()->mode() == nodebank_panel::TRACK && false == m_track_gizmo_using && false == m_straights.dragging && false == m_switch.placing && track_shortcut(Key))
return;
bool handled = true; bool handled = true;
switch (Key) switch (Key)
{ {
@@ -3039,7 +3048,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
if (Action == GLFW_RELEASE) if (Action == GLFW_RELEASE)
return; return;
// shortcuts: undo/redo // shortcuts: undo/redo. not in the middle of a drag, it would go on from the state which was undone
if (Global.ctrlState && (Key == GLFW_KEY_Z || Key == GLFW_KEY_Y) && is_press(Action) && (m_track_gizmo_using || m_straights.dragging || m_extend.active || m_switch.placing))
return;
if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action)) if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action))
{ {
undo_last(); undo_last();
@@ -3127,14 +3138,18 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
{ {
road_cancel(); road_cancel();
} }
// the first Esc drops what is going on in the track mode, the next one goes back to the selection
if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK)
{ {
m_track_point = {}; if (m_lay.active && false == m_lay.points.empty())
m_route.vertex = -1; lay_cancel();
m_straights.handle = -1; else if (track_busy())
m_route.grip = -1; {
m_straights.tool_placed = false; cancel_track_tools();
m_straights.detour.clear(); m_straights.tool = 0;
}
else
show_track_tab(track_tab::path);
} }
break; break;
@@ -3142,6 +3157,18 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in
// area fill: remove the last outline point // area fill: remove the last outline point
if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty())
m_fill_points.pop_back(); m_fill_points.pop_back();
if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active && !m_lay.points.empty())
{
m_lay.points.pop_back();
if (m_lay.points.empty())
m_lay.start = {};
}
break;
case GLFW_KEY_ENTER:
case GLFW_KEY_KP_ENTER:
if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active)
lay_finish({});
break; break;
case GLFW_KEY_K: case GLFW_KEY_K:
@@ -3221,8 +3248,8 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
return; return;
} }
// with the road window open the left button works its tools, whichever edit mode is on // with the road window open the left button works its tools, whichever edit mode is on but the track one
if (m_roadtool.window && Button == GLFW_MOUSE_BUTTON_LEFT) if (m_roadtool.window && Button == GLFW_MOUSE_BUTTON_LEFT && ui()->mode() != nodebank_panel::TRACK)
{ {
if (is_press(Action)) if (is_press(Action))
{ {
@@ -3261,12 +3288,21 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const
if (mode == nodebank_panel::TRACK) if (mode == nodebank_panel::TRACK)
{ {
if (m_lay.active)
{
GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) {
if (viewport_click())
lay_click();
});
m_input.mouse.button(Button, Action);
return;
}
if (start_extend() || start_switch_placement()) if (start_extend() || start_switch_placement())
{ {
m_input.mouse.button(Button, Action); m_input.mouse.button(Button, Action);
return; return;
} }
if ((Mods & GLFW_MOD_ALT) != 0) if ((Mods & GLFW_MOD_ALT) != 0 && m_track_tab == track_tab::straights)
{ {
GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) {
if (viewport_click() && dynamic_cast<TTrack *>(node) != nullptr) if (viewport_click() && dynamic_cast<TTrack *>(node) != nullptr)

View File

@@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorOrthophoto.hpp" #include "editor/editorOrthophoto.hpp"
#include "editor/editorTrack.hpp" #include "editor/editorTrack.hpp"
#include "editor/editorAlignment.hpp" #include "editor/editorAlignment.hpp"
#include "editor/editorSpeed.hpp"
#include "editor/editorRoad.hpp" #include "editor/editorRoad.hpp"
#include "editor/editorProfile.hpp" #include "editor/editorProfile.hpp"
#include "editor/editorInfra.hpp" #include "editor/editorInfra.hpp"
@@ -103,6 +104,7 @@ class editor_mode : public application_mode, private editor_track::observer
bool picking; bool picking;
}; };
struct stored_profile;
struct EditorSnapshot struct EditorSnapshot
{ {
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit }; enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit };
@@ -123,6 +125,8 @@ class editor_mode : public application_mode, private editor_track::observer
std::vector<TTrack *> removed; std::vector<TTrack *> removed;
editor_road::record roads; // what a change of the roads comes to editor_road::record roads; // what a change of the roads comes to
std::vector<infra::object_state> infra; // objects which followed the paths std::vector<infra::object_state> infra; // objects which followed the paths
bool profiles{false}; // the stored grade line designs changed too, these were the ones before
std::vector<stored_profile> profile_library;
}; };
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
@@ -300,19 +304,22 @@ class editor_mode : public application_mode, private editor_track::observer
float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held
TTrack *selected_track() const; TTrack *selected_track() const;
void render_track_ui();
void render_path_ui(); void render_path_ui();
void draw_track_overlay() const; void draw_track_overlay() const;
bool pick_track_handle(); bool pick_track_handle();
void select_track(scene::basic_node *Node); void select_track(scene::basic_node *Node);
void render_track_gizmo(); void render_track_gizmo();
void commit_track_drag(bool const Force); void commit_track_drag(bool const Force);
void cancel_track_tools();
void push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created = {}, std::vector<TTrack *> Removed = {}); void push_track_snapshot(std::vector<std::pair<TTrack *, editor_track::state>> States, std::vector<TTrack *> Created = {}, std::vector<TTrack *> Removed = {});
// false, with the reason, when one of the paths can't be changed, its file not taking the changes back
static bool tracks_editable(std::vector<TTrack *> const &Tracks, std::string &Reason);
void trim_history(); void trim_history();
void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo); void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo);
editor_track::point_ref m_track_point; editor_track::point_ref m_track_point;
std::vector<TTrack *> m_track_drag; std::vector<TTrack *> m_track_drag;
std::vector<std::pair<TTrack *, editor_track::point_ref>> m_track_drag_points; std::vector<std::pair<TTrack *, editor_track::point_ref>> m_track_drag_points;
std::vector<editor_track::joint> m_track_joints; // of the neighbours, following the whole track dragged
editor_track::snap_target m_track_snap; editor_track::snap_target m_track_snap;
glm::dvec3 m_track_pivot{0.0}; glm::dvec3 m_track_pivot{0.0};
glm::mat4 m_track_gizmo{1.0f}; glm::mat4 m_track_gizmo{1.0f};
@@ -322,6 +329,15 @@ class editor_mode : public application_mode, private editor_track::observer
editor_track::state m_track_field_before; editor_track::state m_track_field_before;
std::array<std::array<char, 256>, 3> m_track_materials{}; std::array<std::array<char, 256>, 3> m_track_materials{};
std::array<TTrack const *, 3> m_track_material_edited{}; // path whose texture name is being typed std::array<TTrack const *, 3> m_track_material_edited{}; // path whose texture name is being typed
// typed names of the track circuits, the sleeper model and its skin, and the event to add, for the path
TTrack const *m_track_names_for{nullptr};
std::array<char, 256> m_track_isolated{};
std::array<char, 256> m_track_sleeper_model{};
std::array<char, 256> m_track_sleeper_skin{};
std::array<char, 128> m_track_event{};
int m_track_event_list{0};
std::string m_track_events_missing;
void render_path_circuits(TTrack &Track);
std::string m_track_mode_notice; std::string m_track_mode_notice;
// structure gauge along the line of the selected path, shown in the 3d view // structure gauge along the line of the selected path, shown in the 3d view
struct gauge_state struct gauge_state
@@ -383,9 +399,9 @@ class editor_mode : public application_mode, private editor_track::observer
alignment::result result; alignment::result result;
int vertex{-1}; int vertex{-1};
int grip{-1}; int grip{-1};
bool single{true}; // one curve between its straights, instead of the whole line
}; };
void render_route_ui(); void render_route_ui();
void render_route_ends();
bool render_route_parameters(); bool render_route_parameters();
bool render_route_vertices(); bool render_route_vertices();
bool render_route_vertex(int const Index); bool render_route_vertex(int const Index);
@@ -393,8 +409,12 @@ class editor_mode : public application_mode, private editor_track::observer
void route_reset(); void route_reset();
void route_bind_ends(); void route_bind_ends();
bool route_from_curve(TTrack &Track); bool route_from_curve(TTrack &Track);
bool route_from_line(TTrack &Track);
alignment::vertex route_vertex_of(editor_track::curve const &Curve) const;
bool route_load(TTrack &Track) { return m_route.single ? route_from_curve(Track) : route_from_line(Track); }
void route_update() { m_route.result = alignment::compute(m_route.design); } void route_update() { m_route.result = alignment::compute(m_route.design); }
void route_apply(); void route_apply();
bool route_relay_through_switches(editor_track::chain const &Chain, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created);
void route_recommend(alignment::vertex &Vertex) const; void route_recommend(alignment::vertex &Vertex) const;
void draw_route_overlay() const; void draw_route_overlay() const;
bool pick_route_vertex(); bool pick_route_vertex();
@@ -489,11 +509,13 @@ class editor_mode : public application_mode, private editor_track::observer
std::vector<editor_track::switch_template> templates; std::vector<editor_track::switch_template> templates;
bool collected{false}; bool collected{false};
int armed{-1}; int armed{-1};
int last{0}; // template armed the last time
bool placing{false}; bool placing{false};
double along{0.0}; double along{0.0};
glm::dvec3 point{0.0}; glm::dvec3 point{0.0};
glm::dvec3 mouse{0.0}; glm::dvec3 mouse{0.0};
editor_track::straight line; editor_track::straight line;
std::string status;
}; };
switch_tool m_switch; switch_tool m_switch;
int m_turnout_template{0}; int m_turnout_template{0};
@@ -509,6 +531,31 @@ class editor_mode : public application_mode, private editor_track::observer
bool atend{true}; bool atend{true};
}; };
extend_tool m_extend; extend_tool m_extend;
// lays new track through the clicked points, with no path needed to start from
struct lay_tool
{
bool active{false};
std::vector<glm::dvec3> points;
editor_track::snap_target start; // free end the track starts from, if any
glm::dvec3 mouse{0.0};
editor_track::snap_target mouse_snap;
std::vector<segment_data> preview;
std::string preview_error;
double preview_length{0.0};
double radius{500.0};
bool transitions{true};
double piece_length{100.0};
bool copy_selected{false};
editor_track::path_style style;
std::string status;
};
lay_tool m_lay;
void render_lay_ui();
void render_lay_status();
void lay_click();
void lay_finish(editor_track::snap_target const &End);
void lay_cancel();
std::vector<segment_data> lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const;
void render_switch_ui(); void render_switch_ui();
bool start_switch_placement(); bool start_switch_placement();
void finish_switch_placement(); void finish_switch_placement();
@@ -538,17 +585,44 @@ class editor_mode : public application_mode, private editor_track::observer
bool start_extend(); bool start_extend();
void finish_extend(); void finish_extend();
std::vector<segment_data> extend_pieces() const; std::vector<segment_data> extend_pieces() const;
// the free end of the path as the start of a new track; false if it isn't free or has no direction
static bool free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool);
// the free end new pieces are added at: the selected point if it's one, else the end, else the start
bool chosen_free_end(extend_tool &Tool);
// adds a straight (Side 0) or a curve to the left (1) or the right (-1) at the free end of the selected track
void extend_add(int const Side);
void render_extend_ui();
bool m_extend_freehand{false}; // dragging the ring of a free end draws a curve through the cursor, not a straight
double m_extend_length{25.0};
double m_extend_radius{300.0};
bool extend_snap(editor_track::snap_target &Target) const; bool extend_snap(editor_track::snap_target &Target) const;
void update_build_tools(); void update_build_tools();
void draw_build_overlay() const; void draw_build_overlay() const;
enum class track_tab { straights, route, path, turnout }; // modes of the track editor, one at a time: path is the plain selection
enum class track_tab { straights, route, path, turnout, profile, speed, infra, lay };
struct track_mode
{
char const *label;
char const *key;
track_tab tab;
char const *tooltip;
};
static std::array<track_mode, 8> const &track_modes();
void render_track_menu();
void arm_switch();
bool m_track_window_open{false}; bool m_track_window_open{false};
void render_track_window(); // the inspector is pinned to the right edge of the screen, the profile strip to the bottom one, both resized from the inner edge
void render_track_modes(TTrack &Track); void render_track_inspector();
void render_track_modes(TTrack *Track);
void show_track_tab(track_tab const Tab);
bool track_analysis_tab() const { return m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::infra; }
bool track_shortcut(int const Key);
bool track_busy() const; // something going on in the mode, which Esc drops before it leaves the mode
std::string track_mode_name() const;
void render_turnout_ui(); void render_turnout_ui();
void render_path_parameters(TTrack &Track); void render_path_parameters(TTrack &Track);
void render_straight_ui(); void render_straight_ui();
track_tab m_track_tab{track_tab::straights}; track_tab m_track_tab{track_tab::path};
void delete_selected_track(); void delete_selected_track();
void start_straight_gesture(int const Tool); void start_straight_gesture(int const Tool);
void finish_straight_gesture(); void finish_straight_gesture();
@@ -558,7 +632,8 @@ class editor_mode : public application_mode, private editor_track::observer
char const *key; char const *key;
std::string action; std::string action;
}; };
std::vector<key_hint> track_key_hints() const; std::vector<key_hint> track_key_hints(bool const All) const;
void render_track_guide();
std::string track_readout() const; std::string track_readout() const;
void draw_track_hints(); void draw_track_hints();
bool m_route_tab{true}; bool m_route_tab{true};
@@ -593,6 +668,11 @@ class editor_mode : public application_mode, private editor_track::observer
bool panning{false}; bool panning{false};
bool changed{false}; bool changed{false};
double hover{-1.0}; double hover{-1.0};
// under the cursor in the strip
int hover_point{-1};
int hover_grip{-1};
bool hover_line{false};
int shown{-2}; // point in the side panel
std::string status; std::string status;
std::string error; std::string error;
}; };
@@ -616,7 +696,10 @@ class editor_mode : public application_mode, private editor_track::observer
void profile_forget(); void profile_forget();
void profile_open_stored(std::size_t const Index); void profile_open_stored(std::size_t const Index);
void profile_edited(); void profile_edited();
void render_profile_window(); void render_profile_body();
void render_profile_strip();
void render_profile_toolbar();
void render_profile_points();
void render_profile_source(); void render_profile_source();
bool render_profile_parameters(); bool render_profile_parameters();
void render_profile_issues(); void render_profile_issues();
@@ -630,7 +713,7 @@ class editor_mode : public application_mode, private editor_track::observer
void profile_fit_view(); void profile_fit_view();
void profile_after_undo(); void profile_after_undo();
struct profile_view; struct profile_view;
void render_profile_canvas(); void render_profile_canvas(float const Height);
void profile_fit_exaggeration(profile_view const &View); void profile_fit_exaggeration(profile_view const &View);
void profile_navigate(profile_view const &View, bool const Hovered); void profile_navigate(profile_view const &View, bool const Hovered);
bool profile_canvas_edit(profile_view const &View, bool const Hovered); bool profile_canvas_edit(profile_view const &View, bool const Hovered);
@@ -687,7 +770,32 @@ class editor_mode : public application_mode, private editor_track::observer
void track_split(TTrack &Original, TTrack &Created, segment_data const &First) override; void track_split(TTrack &Original, TTrack &Created, segment_data const &First) override;
void infra_move(infra::binding &Binding); void infra_move(infra::binding &Binding);
void infra_attach(EditorSnapshot &Snapshot); void infra_attach(EditorSnapshot &Snapshot);
void render_infra_window(); void render_infra_body();
// speed limits of the paths checked against what their geometry allows
struct speed_state
{
bool open{false};
int scope{0}; // 0: the line through the selected path, 1: the whole scenery
speed_check::options options;
std::vector<std::pair<TTrack *, int>> queue;
std::size_t next{0};
std::vector<speed_check::verdict> results;
bool show_all{false};
int limits{0}; // 0: all the paths, 1: only those with a speed limit set, 2: only those without
int selected{-1};
std::size_t history{0}; // size of the undo history at the check
std::string status;
};
speed_state m_speed;
void render_speed_body();
void speed_start();
void speed_step();
void speed_focus(int const Index);
// with Branches the limits of the switch branches count too, otherwise they're only warnings
void speed_apply(std::vector<int> const &Indices, bool const Branches);
std::vector<int> speed_listed() const;
void draw_speed_overlay() const;
void infra_rebase(); void infra_rebase();
void render_infra_search(); void render_infra_search();
void render_infra_candidates(); void render_infra_candidates();
@@ -811,7 +919,8 @@ class editor_mode : public application_mode, private editor_track::observer
// Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none // Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none
void road_profile(std::vector<segment_data> &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh); void road_profile(std::vector<segment_data> &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh);
// height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height // height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height
std::vector<double> ground_heights(std::vector<glm::dvec3> const &Points, bool const Fresh); // Found: set for the points which have the ground under them
std::vector<double> ground_heights(std::vector<glm::dvec3> const &Points, bool const Fresh, std::vector<char> *Found = nullptr);
void push_road_snapshot(editor_road::record Record); void push_road_snapshot(editor_road::record Record);
void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo); void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector<EditorSnapshot> &Opposite, bool const Undo);
road_tool m_roadtool; road_tool m_roadtool;

View File

@@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/.
#include "utilities/Logs.h" #include "utilities/Logs.h"
#include "world/Track.h" #include "world/Track.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
#include "utilities/translation.h"
#include <glm/gtc/type_ptr.hpp> #include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <algorithm> #include <algorithm>
@@ -414,12 +415,12 @@ bool edit_points(std::vector<gauge::profile::point> &Points)
bool edited{false}; bool edited{false};
bool inserted{false}; bool inserted{false};
int remove{-1}; int remove{-1};
ImGui::Columns(3, "gaugeoutline", false); ImGui::Columns(3, STR_C("gaugeoutline"), false);
ImGui::SetColumnWidth(0, 110.0f); ImGui::SetColumnWidth(0, 110.0f);
ImGui::SetColumnWidth(1, 110.0f); ImGui::SetColumnWidth(1, 110.0f);
ImGui::TextDisabled("Half-width"); ImGui::TextDisabled(STR_C("Half-width"));
ImGui::NextColumn(); ImGui::NextColumn();
ImGui::TextDisabled("Height"); ImGui::TextDisabled(STR_C("Height"));
ImGui::NextColumn(); ImGui::NextColumn();
ImGui::NextColumn(); ImGui::NextColumn();
for (int i = 0; i < static_cast<int>(Points.size()); ++i) for (int i = 0; i < static_cast<int>(Points.size()); ++i)
@@ -441,7 +442,7 @@ bool edit_points(std::vector<gauge::profile::point> &Points)
inserted = edited = true; inserted = edited = true;
} }
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Insert a point after this one"); ImGui::SetTooltip(STR_C("Insert a point after this one"));
ImGui::SameLine(); ImGui::SameLine();
if (Points.size() > 2 && ImGui::SmallButton("x")) if (Points.size() > 2 && ImGui::SmallButton("x"))
remove = i; remove = i;
@@ -676,48 +677,48 @@ void editor_mode::render_gauge_window()
if (false == m_gauge.open) if (false == m_gauge.open)
return; return;
ImGui::SetNextWindowSize(ImVec2(380.0f, 420.0f), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(380.0f, 420.0f), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin("Structure gauge", &m_gauge.open)) if (false == ImGui::Begin(STR_C("Structure gauge"), &m_gauge.open))
{ {
ImGui::End(); ImGui::End();
return; return;
} }
if (ImGui::Checkbox("Show along the selected path", &m_gauge.enabled)) if (ImGui::Checkbox(STR_C("Show along the selected path"), &m_gauge.enabled))
m_gauge.pending = true; m_gauge.pending = true;
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Translucent tunnel of the gauge 500 m each way along the line of the selected path.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II."); ImGui::SetTooltip(STR_C("Translucent tunnel of the gauge 500 m each way along the line of the selected path.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II."));
gauge_load(); gauge_load();
bool changed{render_gauge_choice("Railway", m_gauge.profile, false)}; bool changed{render_gauge_choice("Railway", m_gauge.profile, false)};
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("GPL: unified structure gauge, widened only in the curves under 250 m.\nG1, G2, GA, GB, GC: limit (GSZ) and nominal (NSZ) installation gauges, widened by 3750/R in all curves."); ImGui::SetTooltip(STR_C("GPL: unified structure gauge, widened only in the curves under 250 m.\nG1, G2, GA, GB, GC: limit (GSZ) and nominal (NSZ) installation gauges, widened by 3750/R in all curves."));
changed |= render_gauge_choice("Road", m_gauge.road, true); changed |= render_gauge_choice("Road", m_gauge.road, true);
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Clearance over the roads, from 0.5 m beyond the edges of the carriageway"); ImGui::SetTooltip(STR_C("Clearance over the roads, from 0.5 m beyond the edges of the carriageway"));
if (m_gauge.scan) if (m_gauge.scan)
{ {
auto const label{format("Scanning the map %.0f%%, %zu found", m_gauge.scan->progress * 100.f, m_gauge.map.hits.size())}; auto const label{format("Scanning the map %.0f%%, %zu found", m_gauge.scan->progress * 100.f, m_gauge.map.hits.size())};
ImGui::ProgressBar(m_gauge.scan->progress, ImVec2(-70.0f, 0.0f), label.c_str()); ImGui::ProgressBar(m_gauge.scan->progress, ImVec2(-70.0f, 0.0f), label.c_str());
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("Cancel")) if (ImGui::SmallButton(STR_C("Cancel")))
finish_gauge_map(); finish_gauge_map();
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Stops the scan and keeps the models found so far"); ImGui::SetTooltip(STR_C("Stops the scan and keeps the models found so far"));
} }
else if (ImGui::Button("Find the violations on the whole map")) else if (ImGui::Button(STR_C("Find the violations on the whole map")))
scan_gauge_map(); scan_gauge_map();
else if (ImGui::IsItemHovered()) else if (ImGui::IsItemHovered())
ImGui::SetTooltip("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes"); ImGui::SetTooltip(STR_C("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes"));
if (m_gauge.map.scanned && false == static_cast<bool>(m_gauge.scan)) if (m_gauge.map.scanned && false == static_cast<bool>(m_gauge.scan))
{ {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("Clear")) if (ImGui::SmallButton(STR_C("Clear")))
{ {
m_gauge.map = {}; m_gauge.map = {};
m_gauge.current = -1; m_gauge.current = -1;
m_gauge.published = false; m_gauge.published = false;
} }
else if (m_gauge.map.history != m_history.size()) else if (m_gauge.map.history != m_history.size())
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "The scenery changed since the scan (%s)", m_gauge.map.profile.c_str()); ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str());
} }
render_gauge_hits(); render_gauge_hits();
changed |= render_gauge_outline(m_gauge.profile); changed |= render_gauge_outline(m_gauge.profile);
@@ -734,10 +735,10 @@ void editor_mode::render_gauge_hits()
return; return;
if (hits.empty()) if (hits.empty())
{ {
ImGui::TextDisabled(m_gauge.map.scanned ? "No model enters the gauge on the map" : "The gauge is clear along this line"); ImGui::TextDisabled(m_gauge.map.scanned ? STR_C("No model enters the gauge on the map") : STR_C("The gauge is clear along this line"));
return; return;
} }
ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%zu models enter the gauge%s", hits.size(), m_gauge.map.scanned ? " on the map" : ""); ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), STR_C("%zu models enter the gauge%s"), hits.size(), m_gauge.map.scanned ? STR_C(" on the map") : "");
if (ImGui::ArrowButton("##gaugeprevious", ImGuiDir_Left)) if (ImGui::ArrowButton("##gaugeprevious", ImGuiDir_Left))
gauge_focus(m_gauge.current - 1); gauge_focus(m_gauge.current - 1);
ImGui::SameLine(); ImGui::SameLine();
@@ -762,7 +763,7 @@ bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const
{ {
bool changed{false}; bool changed{false};
ImGui::SetNextItemWidth(-60.0f); ImGui::SetNextItemWidth(-60.0f);
if (ImGui::BeginCombo(Label, Index >= 0 ? m_gauge.profiles[Index].name.c_str() : "none")) if (ImGui::BeginCombo(Label, Index >= 0 ? m_gauge.profiles[Index].name.c_str() : STR_C("none")))
{ {
for (int i = 0; i < static_cast<int>(m_gauge.profiles.size()); ++i) for (int i = 0; i < static_cast<int>(m_gauge.profiles.size()); ++i)
if ((m_gauge.profiles[i].kind == gauge::kind::road) == Road && ImGui::Selectable(m_gauge.profiles[i].name.c_str(), i == Index)) if ((m_gauge.profiles[i].kind == gauge::kind::road) == Road && ImGui::Selectable(m_gauge.profiles[i].name.c_str(), i == Index))
@@ -780,12 +781,12 @@ bool editor_mode::render_gauge_outline(int const Index)
if (Index < 0) if (Index < 0)
return false; return false;
auto &profile{m_gauge.profiles[Index]}; auto &profile{m_gauge.profiles[Index]};
if (false == ImGui::TreeNode(&profile, "Outline of %s (right half)", profile.name.c_str())) if (false == ImGui::TreeNode(&profile, STR_C("Outline of %s (right half)"), profile.name.c_str()))
return false; return false;
ImGui::TextDisabled(profile.kind == gauge::kind::road ? "Metres from the edge of the carriageway and above the road, bottom up." ImGui::TextDisabled(profile.kind == gauge::kind::road ? STR_C("Metres from the edge of the carriageway and above the road, bottom up.")
: "PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); : "PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up.");
bool edited{edit_points(profile.outline)}; bool edited{edit_points(profile.outline)};
if (ImGui::SmallButton("Restore the starting outlines")) if (ImGui::SmallButton(STR_C("Restore the starting outlines")))
{ {
m_gauge.profiles = gauge::default_profiles(); m_gauge.profiles = gauge::default_profiles();
gauge_load(); gauge_load();

View File

@@ -27,6 +27,7 @@ http://mozilla.org/MPL/2.0/.
#include "world/Traction.h" #include "world/Traction.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
#include "utilities/translation.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
@@ -707,7 +708,7 @@ void editor_mode::infra_recognize()
state.tracks = infra_scope(); state.tracks = infra_scope();
if (state.tracks.empty()) if (state.tracks.empty())
{ {
state.error = (state.scope == 2 ? "Open the vertical profile of a route first" : state.scope == 3 ? "The scenery has no paths the editor can handle" : "Select a path first"); state.error = (state.scope == 2 ? STR_C("Open the vertical profile of a route first") : state.scope == 3 ? STR_C("The scenery has no paths the editor can handle") : STR_C("Select a path first"));
return; return;
} }
track_index index; track_index index;
@@ -803,7 +804,7 @@ void editor_mode::infra_recognize()
add(binding, "nearby", corridor); add(binding, "nearby", corridor);
} }
std::stable_sort(state.candidates.begin(), state.candidates.end(), [](infra_candidate const &A, infra_candidate const &B) { return A.binding.group < B.binding.group; }); std::stable_sort(state.candidates.begin(), state.candidates.end(), [](infra_candidate const &A, infra_candidate const &B) { return A.binding.group < B.binding.group; });
state.status = format("%d object(s) found along %d path(s)", static_cast<int>(state.candidates.size()), static_cast<int>(state.tracks.size())); state.status = format(STR_C("%d object(s) found along %d path(s)"), static_cast<int>(state.candidates.size()), static_cast<int>(state.tracks.size()));
} }
void editor_mode::infra_bind_chosen() void editor_mode::infra_bind_chosen()
@@ -823,7 +824,7 @@ void editor_mode::infra_bind_chosen()
candidate.chosen = false; candidate.chosen = false;
++count; ++count;
} }
state.status = format("%d object(s) bound, they follow the changes of the track. The bindings are kept in the scenery files on save", count); state.status = format(STR_C("%d object(s) bound, they follow the changes of the track. The bindings are kept in the scenery files on save"), count);
} }
void editor_mode::infra_unbind(std::vector<std::size_t> Indices) void editor_mode::infra_unbind(std::vector<std::size_t> Indices)
@@ -1108,7 +1109,7 @@ void editor_mode::infra_attach(EditorSnapshot &Snapshot)
return true; return true;
for (auto const &anchor : State.saved.anchors) for (auto const &anchor : State.saved.anchors)
{ {
if (contains(Snapshot.created, anchor.at.track)) if (contains(Snapshot.created, anchor.at.track) || contains(Snapshot.removed, anchor.at.track))
return true; return true;
for (auto const &entry : Snapshot.tracks) for (auto const &entry : Snapshot.tracks)
if (entry.first == anchor.at.track) if (entry.first == anchor.at.track)
@@ -1122,37 +1123,29 @@ void editor_mode::infra_attach(EditorSnapshot &Snapshot)
m_infra_buffer.clear(); m_infra_buffer.clear();
} }
void editor_mode::render_infra_window() void editor_mode::render_infra_body()
{ {
auto &state{m_infra}; auto &state{m_infra};
if (false == state.open)
return;
ImGui::SetNextWindowSize(ImVec2(560, 620), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin("Infrastructure along the track###trackinfra", &state.open))
{
ImGui::End();
return;
}
infra_load(); infra_load();
Live.refresh(); Live.refresh();
auto follow{state.follow}; auto follow{state.follow};
if (ImGui::Checkbox("Bound objects follow the changes of the track", &follow)) if (ImGui::Checkbox(STR_C("Bound objects follow the changes of the track"), &follow))
{ {
state.follow = follow; state.follow = follow;
if (follow) if (follow)
infra_rebase(); infra_rebase();
} }
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("Bound objects keep their chainage along the path, the offset from its axis and the height above the rail top;\n" ImGui::SetTooltip(STR_C("Bound objects keep their chainage along the path, the offset from its axis and the height above the rail top;\n"
"models and includes turn with the path and stay upright. The move is a part of the undo step of the change."); "models and includes turn with the path and stay upright. The move is a part of the undo step of the change."));
ImGui::SameLine(); ImGui::SameLine();
ImGui::Checkbox("Show in 3D", &state.show); ImGui::Checkbox(STR_C("Show in 3D"), &state.show);
ImGui::Text("Objects bound in the scenery: %d", static_cast<int>(m_bindings.size())); ImGui::Text(STR_C("Objects bound in the scenery: %d"), static_cast<int>(m_bindings.size()));
if (false == m_bindings_unresolved.empty()) if (false == m_bindings_unresolved.empty())
{ {
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextColored(ImVec4(1.f, 0.8f, 0.3f, 1.f), "(%d stored binding(s) not found, kept as they are)", static_cast<int>(m_bindings_unresolved.size())); ImGui::TextColored(ImVec4(1.f, 0.8f, 0.3f, 1.f), STR_C("(%d stored binding(s) not found, kept as they are)"), static_cast<int>(m_bindings_unresolved.size()));
} }
ImGui::Separator(); ImGui::Separator();
@@ -1163,7 +1156,6 @@ void editor_mode::render_infra_window()
ImGui::TextColored(ImVec4(1.f, 0.4f, 0.35f, 1.f), "%s", state.error.c_str()); ImGui::TextColored(ImVec4(1.f, 0.4f, 0.35f, 1.f), "%s", state.error.c_str());
else if (false == state.status.empty()) else if (false == state.status.empty())
ImGui::TextWrapped("%s", state.status.c_str()); ImGui::TextWrapped("%s", state.status.c_str());
ImGui::End();
} }
// what changed while the bindings did not follow the track becomes their new starting point // what changed while the bindings did not follow the track becomes their new starting point
@@ -1199,15 +1191,15 @@ void editor_mode::render_infra_search()
if (state.scope == 1) if (state.scope == 1)
{ {
ImGui::SetNextItemWidth(120.f); ImGui::SetNextItemWidth(120.f);
ImGui::InputFloat("Reach each way, m", &state.reach, 100.f, 500.f, "%.0f"); ImGui::InputFloat(STR_C("Reach each way, m"), &state.reach, 100.f, 500.f, "%.0f");
state.reach = std::clamp(state.reach, 10.f, 20000.f); state.reach = std::clamp(state.reach, 10.f, 20000.f);
} }
ImGui::SetNextItemWidth(120.f); ImGui::SetNextItemWidth(120.f);
ImGui::InputFloat("Corridor each side of the axis, m", &state.corridor, 0.5f, 2.f, "%.1f"); ImGui::InputFloat(STR_C("Corridor each side of the axis, m"), &state.corridor, 0.5f, 2.f, "%.1f");
state.corridor = std::clamp(state.corridor, 0.5f, 50.f); state.corridor = std::clamp(state.corridor, 0.5f, 50.f);
if (ImGui::TreeNode("Rules for names of the files")) if (ImGui::TreeNode(STR_C("Rules for names of the files")))
{ {
ImGui::TextDisabled("Parts of the names, separated by spaces; the first rule which matches decides"); ImGui::TextDisabled(STR_C("Parts of the names, separated by spaces; the first rule which matches decides"));
for (std::size_t i = 0; i < state.rules.size(); ++i) for (std::size_t i = 0; i < state.rules.size(); ++i)
{ {
auto &rule{state.rules[i]}; auto &rule{state.rules[i]};
@@ -1217,13 +1209,13 @@ void editor_mode::render_infra_search()
if (ImGui::InputText((std::string{infra::name(rule.group)} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer))) if (ImGui::InputText((std::string{infra::name(rule.group)} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer)))
rule.keywords = buffer; rule.keywords = buffer;
} }
if (ImGui::SmallButton("Default rules")) if (ImGui::SmallButton(STR_C("Default rules")))
state.rules = infra::default_rules(); state.rules = infra::default_rules();
ImGui::TextDisabled("Includes are matched by the template name and its //$e description, models by the file and node name;\n" ImGui::TextDisabled(STR_C("Includes are matched by the template name and its //$e description, models by the file and node name;\n"
"traction is always the catenary, memory cells and event launchers the logic"); "traction is always the catenary, memory cells and event launchers the logic"));
ImGui::TreePop(); ImGui::TreePop();
} }
if (ImGui::Button("Find objects")) if (ImGui::Button(STR_C("Find objects")))
infra_recognize(); infra_recognize();
} }
@@ -1244,13 +1236,13 @@ void editor_mode::render_infra_candidates()
for (auto const &candidate : candidates) for (auto const &candidate : candidates)
chosen += candidate.chosen ? 1 : 0; chosen += candidate.chosen ? 1 : 0;
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button(format("Bind the chosen (%d)", chosen).c_str())) if (ImGui::Button(format(STR_C("Bind the chosen (%d)"), chosen).c_str()))
infra_bind_chosen(); infra_bind_chosen();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("All")) if (ImGui::SmallButton(STR_C("All")))
choose({}, true); choose({}, true);
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("None")) if (ImGui::SmallButton(STR_C("None")))
choose({}, false); choose({}, false);
ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true);
@@ -1263,10 +1255,10 @@ void editor_mode::render_infra_candidates()
auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast<int>(count)).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast<int>(count)).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)};
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton((std::string{"all##g"} + infra::name(group)).c_str())) if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str()))
choose(group, true); choose(group, true);
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton((std::string{"none##g"} + infra::name(group)).c_str())) if (ImGui::SmallButton((std::string{STR_C("none##g")} + infra::name(group)).c_str()))
choose(group, false); choose(group, false);
if (false == open) if (false == open)
continue; continue;
@@ -1281,7 +1273,7 @@ void editor_mode::render_infra_candidates()
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
if (candidate.bound) if (candidate.bound)
{ {
ImGui::TextDisabled("[bound]"); ImGui::TextDisabled(STR_C("[bound]"));
} }
else else
{ {
@@ -1292,7 +1284,7 @@ void editor_mode::render_infra_candidates()
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
{ {
state.hovered = static_cast<int>(i); state.hovered = static_cast<int>(i);
ImGui::SetTooltip("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s", infra::name(candidate.binding.type), candidate.binding.anchors.front().at.track->name().c_str(), ImGui::SetTooltip(STR_C("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s"), infra::name(candidate.binding.type), candidate.binding.anchors.front().at.track->name().c_str(),
std::abs(candidate.offset), candidate.offset >= 0.0 ? "right" : "left", candidate.binding.anchors.front().height, candidate.reason.c_str()); std::abs(candidate.offset), candidate.offset >= 0.0 ? "right" : "left", candidate.binding.anchors.front().height, candidate.reason.c_str());
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -1307,7 +1299,7 @@ void editor_mode::render_infra_candidates()
void editor_mode::render_infra_bound() void editor_mode::render_infra_bound()
{ {
if (ImGui::TreeNode("Bound objects along the scope")) if (ImGui::TreeNode(STR_C("Bound objects along the scope")))
{ {
auto const scope{infra_scope()}; auto const scope{infra_scope()};
std::unordered_set<TTrack const *> const tracks(scope.begin(), scope.end()); std::unordered_set<TTrack const *> const tracks(scope.begin(), scope.end());
@@ -1315,11 +1307,11 @@ void editor_mode::render_infra_bound()
for (std::size_t i = 0; i < m_bindings.size(); ++i) for (std::size_t i = 0; i < m_bindings.size(); ++i)
if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; }))
along.push_back(i); along.push_back(i);
ImGui::Text("%d of %d", static_cast<int>(along.size()), static_cast<int>(m_bindings.size())); ImGui::Text(STR_C("%d of %d"), static_cast<int>(along.size()), static_cast<int>(m_bindings.size()));
if (false == along.empty()) if (false == along.empty())
{ {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("Unbind all of these")) if (ImGui::SmallButton(STR_C("Unbind all of these")))
{ {
infra_unbind(along); infra_unbind(along);
along.clear(); along.clear();
@@ -1334,11 +1326,11 @@ void editor_mode::render_infra_bound()
auto const index{along[row]}; auto const index{along[row]};
auto const &binding{m_bindings[index]}; auto const &binding{m_bindings[index]};
ImGui::PushID(static_cast<int>(index)); ImGui::PushID(static_cast<int>(index));
if (ImGui::SmallButton("unbind")) if (ImGui::SmallButton(STR_C("unbind")))
unbind = index; unbind = index;
ImGui::SameLine(); ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(binding.lost ? IM_COL32(255, 80, 70, 255) : colour(binding.group))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(binding.lost ? IM_COL32(255, 80, 70, 255) : colour(binding.group)));
ImGui::Text("%s%s", binding.label.c_str(), binding.lost ? " (no path to follow)" : ""); ImGui::Text("%s%s", binding.label.c_str(), binding.lost ? STR_C(" (no path to follow)") : "");
ImGui::PopStyleColor(); ImGui::PopStyleColor();
ImGui::PopID(); ImGui::PopID();
} }

View File

@@ -17,6 +17,7 @@ http://mozilla.org/MPL/2.0/.
#include "scene/scenelayers.h" #include "scene/scenelayers.h"
#include "simulation/simulation.h" #include "simulation/simulation.h"
#include "utilities/Logs.h" #include "utilities/Logs.h"
#include "utilities/translation.h"
#include "imgui/imgui.h" #include "imgui/imgui.h"
#include <algorithm> #include <algorithm>
@@ -40,6 +41,17 @@ ImU32 const kLabel{IM_COL32(200, 200, 200, 200)};
ImU32 const kSwitch{IM_COL32(160, 100, 220, 45)}; ImU32 const kSwitch{IM_COL32(160, 100, 220, 45)};
ImU32 const kError{IM_COL32(255, 80, 70, 255)}; ImU32 const kError{IM_COL32(255, 80, 70, 255)};
ImU32 const kWarning{IM_COL32(255, 200, 60, 255)}; ImU32 const kWarning{IM_COL32(255, 200, 60, 255)};
ImU32 const kPlan{IM_COL32(120, 190, 255, 255)};
ImU32 const kSwitchLabel{IM_COL32(200, 160, 255, 220)};
ImU32 const kChip{IM_COL32(14, 15, 19, 210)};
// text on a dark plate, readable over the lines of the plot
void plot_label(ImDrawList &Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text)
{
auto const size{ImGui::CalcTextSize(Text.c_str())};
Draw.AddRectFilled(ImVec2(At.x - 3.0f, At.y - 1.0f), ImVec2(At.x + size.x + 3.0f, At.y + size.y + 1.0f), kChip, 3.0f);
Draw.AddText(At, Colour, Text.c_str());
}
double nice_step(double const Approximate) double nice_step(double const Approximate)
{ {
@@ -234,7 +246,7 @@ bool editor_mode::profile_restore()
state.selected = -1; state.selected = -1;
state.changed = false; state.changed = false;
profile_check(); profile_check();
state.status = "Design read from the scenery file " + scene::Layers.layer(entry.layer).name + format(", largest departure from the track %.3f m", state.departure); state.status = STR_C("Design read from the scenery file ") + scene::Layers.layer(entry.layer).name + format(STR_C(", largest departure from the track %.3f m"), state.departure);
return true; return true;
} }
@@ -245,7 +257,7 @@ void editor_mode::profile_store()
return; return;
if (scene::Layers.empty()) if (scene::Layers.empty())
{ {
state.error = "The scenery wasn't opened for editing, the design can't be kept in its files"; state.error = STR_C("The scenery wasn't opened for editing, the design can't be kept in its files");
return; return;
} }
profile_library_load(); profile_library_load();
@@ -291,7 +303,7 @@ void editor_mode::profile_forget()
auto const layer{m_profiles[index].layer}; auto const layer{m_profiles[index].layer};
m_profiles.erase(m_profiles.begin() + index); m_profiles.erase(m_profiles.begin() + index);
profile_library_mark({layer}); profile_library_mark({layer});
state.status = "The design will be removed from " + scene::Layers.layer(layer).name + " on save"; state.status = STR_C("The design will be removed from ") + scene::Layers.layer(layer).name + STR_C(" on save");
} }
void editor_mode::profile_open_stored(std::size_t const Index) void editor_mode::profile_open_stored(std::size_t const Index)
@@ -330,7 +342,7 @@ void editor_mode::profile_open_stored(std::size_t const Index)
return; return;
} }
state.open = true; state.open = true;
state.error = "The route of the stored design isn't in the scenery any more (paths moved or removed)"; state.error = STR_C("The route of the stored design isn't in the scenery any more (paths moved or removed)");
} }
void editor_mode::profile_edited() void editor_mode::profile_edited()
@@ -359,7 +371,7 @@ void editor_mode::profile_open_run(TTrack &Track)
auto route{editor_track::run_route(Track, state.run_length)}; auto route{editor_track::run_route(Track, state.run_length)};
if (route.spans.empty()) if (route.spans.empty())
{ {
state.error = "The path can't carry a profile"; state.error = STR_C("The path can't carry a profile");
return; return;
} }
auto *from{route.spans.front().track}; auto *from{route.spans.front().track};
@@ -386,13 +398,15 @@ void editor_mode::profile_resample()
state.samples = editor_track::sample_route(state.route, 1.0); state.samples = editor_track::sample_route(state.route, 1.0);
auto const &samples{state.samples}; auto const &samples{state.samples};
state.terrain.assign(samples.size(), std::numeric_limits<double>::quiet_NaN()); state.terrain.assign(samples.size(), std::numeric_limits<double>::quiet_NaN());
std::vector<glm::dvec3> points;
points.reserve(samples.size());
for (auto const &sample : samples)
points.push_back(sample.position);
std::vector<char> found;
auto const heights{ground_heights(points, true, &found)};
for (std::size_t i = 0; i < samples.size(); ++i) for (std::size_t i = 0; i < samples.size(); ++i)
{ if (found[i] != 0)
auto const &position{samples[i].position}; state.terrain[i] = heights[i];
auto *patch{terrain_at(position.x, position.z)};
if (patch != nullptr && patch->contains(position.x, position.z))
state.terrain[i] = patch->height_at(position.x, position.z);
}
auto &context{state.context}; auto &context{state.context};
context = {}; context = {};
@@ -425,7 +439,7 @@ void editor_mode::profile_recognize()
state.line.speed = route_speed(state.route); state.line.speed = route_speed(state.route);
state.selected = -1; state.selected = -1;
profile_check(); profile_check();
state.status = format("Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size(), state.departure); state.status = format(STR_C("Grade line recognized from the track: %zu points, largest departure %.3f m"), state.line.points.size(), state.departure);
} }
void editor_mode::profile_check() void editor_mode::profile_check()
@@ -480,7 +494,15 @@ void editor_mode::profile_apply()
return; return;
if (has_errors(state.issues)) if (has_errors(state.issues))
{ {
state.error = "Correct the errors to apply the grade line"; state.error = STR_C("Correct the errors to apply the grade line");
return;
}
std::vector<TTrack *> tracks;
for (auto const &span : state.route.spans)
tracks.push_back(span.track);
if (false == tracks_editable(tracks, state.error))
{
state.error = STR_C("The grade line can't be applied, ") + state.error;
return; return;
} }
auto const line{state.line}; auto const line{state.line};
@@ -490,18 +512,35 @@ void editor_mode::profile_apply()
state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)}; state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)};
auto const changed{states.size()}; auto const changed{states.size()};
auto const added{created.size()}; auto const added{created.size()};
// the stored design changes with the paths, an undo takes both back
profile_library_load();
auto library{m_profiles};
bool const pushed{false == states.empty() || false == created.empty()};
push_track_snapshot(std::move(states), std::move(created)); push_track_snapshot(std::move(states), std::move(created));
if (pushed)
{
m_history.back().profiles = true;
m_history.back().profile_library = std::move(library);
}
state.from = state.route.spans.front().track; state.from = state.route.spans.front().track;
state.to = state.route.spans.back().track; state.to = state.route.spans.back().track;
profile_resample(); profile_resample();
profile_store(); profile_store();
state.status = format("Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed, added, state.departure); state.status = format(STR_C("Grade line applied: %zu paths changed, %zu added, largest departure %.3f m"), changed, added, state.departure);
if (false == scene::Layers.empty()) if (false == scene::Layers.empty())
state.status += "\nThe paths and the design (//$p lines) go to the scenery files on save"; state.status += "\nThe paths and the design (//$p lines) go to the scenery files on save";
if (false == gaps.empty()) auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); };
state.status += "\nSwitch ends off the adjoining paths, to be corrected:"; auto const closed{std::count_if(gaps.begin(), gaps.end(), [](auto const &Gap) { return Gap.closed; })};
if (closed > 0)
state.status += "\nPaths off the route brought to the height of the switches:";
for (auto const &gap : gaps) for (auto const &gap : gaps)
state.status += format("\n %s -> %s: %+.3f m", gap.track->name().c_str(), gap.neighbour->name().c_str(), gap.gap); if (gap.closed)
state.status += format("\n %s at %s: %+.3f m", name(gap.neighbour).c_str(), name(gap.track).c_str(), -gap.gap);
if (closed < static_cast<long>(gaps.size()))
state.status += "\nSwitch ends off the adjoining paths, which can't be changed here:";
for (auto const &gap : gaps)
if (false == gap.closed)
state.status += format("\n %s -> %s: %+.3f m", name(gap.track).c_str(), name(gap.neighbour).c_str(), gap.gap);
WriteLog("Editor: vertical profile - " + state.status, logtype::generic); WriteLog("Editor: vertical profile - " + state.status, logtype::generic);
} }
@@ -527,53 +566,87 @@ void editor_mode::profile_after_undo()
profile_resample(); profile_resample();
} }
void editor_mode::render_profile_window() void editor_mode::render_profile_body()
{ {
auto &state{m_profile}; auto &state{m_profile};
if (false == state.open)
return;
ImGui::SetNextWindowSize(ImVec2(960.0f, 640.0f), ImGuiCond_FirstUseEver);
if (false == ImGui::Begin("Vertical profile###trackprofile", &state.open))
{
ImGui::End();
return;
}
render_profile_source(); render_profile_source();
if (state.route.spans.empty()) if (state.route.spans.empty())
{ {
ImGui::TextDisabled("Select a path in the viewport, then build the route along the line, or set its start and end paths (switches are passed through)"); ImGui::TextWrapped("%s", STR_C("Select a path in the view, then build the route along the line, or set its start and end paths (switches are passed through)"));
if (state.picked_from != nullptr) if (state.picked_from != nullptr)
ImGui::Text("Start: %s", state.picked_from->name().c_str()); ImGui::Text(STR_C("Start: %s"), state.picked_from->name().c_str());
if (false == state.error.empty()) if (false == state.error.empty())
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str());
ImGui::End();
return; return;
} }
ImGui::Text("Route: %zu paths, %.2f m, %s ... %s", state.route.spans.size(), state.route.length, state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str()); ImGui::Separator();
ImGui::TextWrapped(STR_C("Route: %zu paths, %.2f m, %s ... %s"), state.route.spans.size(), state.route.length, state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str());
{ {
bool reversed; bool reversed;
auto const stored{profile_find(reversed)}; auto const stored{profile_find(reversed)};
if (stored >= 0) if (stored >= 0)
{ {
ImGui::TextDisabled("Design kept in %s%s", scene::Layers.layer(m_profiles[stored].layer).name.c_str(), state.changed ? ", changes go there on save" : ""); ImGui::TextDisabled(STR_C("Design kept in %s%s"), scene::Layers.layer(m_profiles[stored].layer).name.c_str(), state.changed ? STR_C(", changes go there on save") : "");
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("Forget the design")) if (ImGui::SmallButton(STR_C("Forget the design")))
profile_forget(); profile_forget();
} }
else if (state.changed) else if (state.changed)
ImGui::TextDisabled("The design goes to the scenery file of the route on save"); ImGui::TextDisabled(STR_C("The design goes to the scenery file of the route on save"));
} }
if (render_profile_parameters()) if (render_profile_parameters())
profile_edited(); profile_edited();
}
render_profile_canvas(); void editor_mode::render_profile_strip()
{
ImGui::Columns(2, "##profilecolumns", true); auto &state{m_profile};
render_profile_point(); auto const &io{ImGui::GetIO()};
ImGui::NextColumn(); ImGui::SetNextWindowPos(ImVec2(8.0f, io.DisplaySize.y - 360.0f), ImGuiCond_FirstUseEver);
render_profile_issues(); ImGui::SetNextWindowSize(ImVec2(std::max(600.0f, io.DisplaySize.x - 500.0f), 320.0f), ImGuiCond_FirstUseEver);
ImGui::Columns(1); auto const title{std::string{STR_C("Vertical profile")} + format(" %.2f m###profilestrip", state.route.length)};
if (ImGui::Begin(title.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
{
auto const side{std::clamp(ImGui::GetContentRegionAvail().x * 0.28f, 280.0f, 420.0f)};
ImGui::BeginChild("##profileplot", ImVec2(-side, 0.0f), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
render_profile_toolbar();
render_profile_canvas(ImGui::GetContentRegionAvail().y);
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("##profileside", ImVec2(0.0f, 0.0f), false);
if (ImGui::BeginTabBar("##profiletabs"))
{
// a click on a point in the plot brings its tab forward
bool const picked{state.selected >= 0 && state.selected != state.shown};
state.shown = state.selected;
if (ImGui::BeginTabItem(STR_C("Point"), nullptr, picked ? ImGuiTabItemFlags_SetSelected : 0))
{
render_profile_point();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem(format("%s (%zu)###profilepoints", STR_C("Points"), state.line.points.size()).c_str()))
{
ImGui::BeginChild("##profilepointlist");
render_profile_points();
ImGui::EndChild();
ImGui::EndTabItem();
}
bool const errors{has_errors(state.issues)};
if (false == state.issues.empty())
ImGui::PushStyleColor(ImGuiCol_Text, errors ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f));
bool const remarks{ImGui::BeginTabItem(format("%s (%zu)###profileremarks", STR_C("Remarks"), state.issues.size()).c_str())};
if (false == state.issues.empty())
ImGui::PopStyleColor();
if (remarks)
{
render_profile_issues();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::EndChild();
}
ImGui::End(); ImGui::End();
} }
@@ -581,30 +654,29 @@ void editor_mode::render_profile_source()
{ {
auto &state{m_profile}; auto &state{m_profile};
auto *selected{selected_track()}; auto *selected{selected_track()};
if (ImGui::Button("Along the line from the selected path") && selected != nullptr) if (ImGui::Button(STR_C("Along the line from the selected path")) && selected != nullptr)
profile_open_run(*selected); profile_open_run(*selected);
ImGui::SameLine(); ImGui::SameLine();
ImGui::PushItemWidth(70.0f); ImGui::PushItemWidth(70.0f);
ImGui::InputDouble("m each way##profilerun", &state.run_length, 0.0, 0.0, "%.0f"); ImGui::DragScalar(STR_C("m each way"), ImGuiDataType_Double, &state.run_length, 10.0f, nullptr, nullptr, "%.0f");
ImGui::PopItemWidth(); ImGui::PopItemWidth();
state.run_length = std::clamp(state.run_length, 10.0, 50000.0); state.run_length = std::clamp(state.run_length, 10.0, 50000.0);
ImGui::SameLine(); if (ImGui::Button(STR_C("Start: selected path")) && selected != nullptr)
if (ImGui::Button("Start: selected path") && selected != nullptr)
state.picked_from = selected; state.picked_from = selected;
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("End: selected path") && selected != nullptr && state.picked_from != nullptr) if (ImGui::Button(STR_C("End: selected path")) && selected != nullptr && state.picked_from != nullptr)
profile_open(state.picked_from, selected); profile_open(state.picked_from, selected);
profile_library_load(); profile_library_load();
if (false == m_profiles.empty() && ImGui::TreeNode("##profilestored", "Designs kept in the scenery (%zu)", m_profiles.size())) if (false == m_profiles.empty() && ImGui::TreeNode("##profilestored", STR_C("Designs kept in the scenery (%zu)"), m_profiles.size()))
{ {
for (std::size_t i = 0; i < m_profiles.size(); ++i) for (std::size_t i = 0; i < m_profiles.size(); ++i)
{ {
auto const &entry{m_profiles[i]}; auto const &entry{m_profiles[i]};
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
if (ImGui::SmallButton("Open")) if (ImGui::SmallButton(STR_C("Open")))
profile_open_stored(i); profile_open_stored(i);
ImGui::SameLine(); ImGui::SameLine();
ImGui::Text("%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)", scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size(), entry.start.x, entry.start.z, entry.end.x, entry.end.z); ImGui::Text(STR_C("%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)"), scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size(), entry.start.x, entry.start.z, entry.end.x, entry.end.z);
ImGui::PopID(); ImGui::PopID();
} }
ImGui::TreePop(); ImGui::TreePop();
@@ -616,77 +688,91 @@ bool editor_mode::render_profile_parameters()
auto &state{m_profile}; auto &state{m_profile};
auto &line{state.line}; auto &line{state.line};
bool changed{false}; bool changed{false};
ImGui::PushItemWidth(90.0f); auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); };
ImGui::InputDouble("Chainage of the start (m)", &state.origin, 0.0, 0.0, "%.2f"); ImGui::PushItemWidth(120.0f);
ImGui::SameLine(); drag("Chainage of the start (m)", state.origin, 1.0f, "%.2f");
changed |= ImGui::InputDouble("Design speed (km/h)", &line.speed, 0.0, 0.0, "%.0f"); changed |= drag("Design speed (km/h)", line.speed, 1.0f, "%.0f");
ImGui::SameLine();
ImGui::SliderFloat("Exaggeration##profile", &state.exaggeration, 1.0f, 500.0f, "1:%.0f", 2.0f);
ImGui::PopItemWidth(); ImGui::PopItemWidth();
line.speed = std::max(1.0, line.speed); line.speed = std::max(1.0, line.speed);
ImGui::Checkbox("Track", &state.show_track); auto const join_tooltip = [] {
ImGui::SameLine(); if (ImGui::IsItemHovered())
ImGui::Checkbox("Terrain", &state.show_terrain); ImGui::SetTooltip("%s", STR_C("Keeps the elevation of the joint and takes over the grade of the adjoining track,\nwith a vertical curve which starts right at the joint"));
ImGui::SameLine(); };
ImGui::Checkbox("Plan curvature", &state.show_plan); if (state.context.start_joined)
ImGui::SameLine();
if (ImGui::Button("Fit the view"))
profile_fit_view();
ImGui::SameLine();
if (ImGui::Button("Recognize from the track"))
{ {
profile_recognize(); changed |= ImGui::Checkbox(STR_C("Join the adjoining track at the start"), &line.join_start);
state.changed = true; join_tooltip();
} }
ImGui::SameLine(); if (state.context.end_joined)
if (ImGui::Button("Apply to the scenery")) {
profile_apply(); changed |= ImGui::Checkbox(STR_C("Join the adjoining track at the end"), &line.join_end);
if (ImGui::TreeNode("Limits##profile")) join_tooltip();
}
if (ImGui::TreeNode(STR_C("Limits")))
{ {
auto &norms{line.norms}; auto &norms{line.norms};
ImGui::PushItemWidth(90.0f); ImGui::PushItemWidth(90.0f);
changed |= ImGui::InputDouble("Maximum grade (per mille)", &norms.grade_max, 0.0, 0.0, "%.2f"); changed |= drag("Maximum grade (per mille)", norms.grade_max, 0.1f, "%.2f");
changed |= ImGui::InputDouble("Maximum grade over switches (per mille)", &norms.grade_switch_max, 0.0, 0.0, "%.2f"); changed |= drag("Maximum grade over switches (per mille)", norms.grade_switch_max, 0.1f, "%.2f");
changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.radius_factor, 0.0, 0.0, "%.2f"); changed |= drag("Vertical curve R >= k*V^2, k", norms.radius_factor, 0.01f, "%.2f");
changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.radius_min, 0.0, 0.0, "%.0f"); changed |= drag("Vertical curve R min (m)", norms.radius_min, 10.0f, "%.0f");
changed |= ImGui::InputDouble("Vertical curve needed above a change of (per mille)", &norms.curve_threshold, 0.0, 0.0, "%.2f"); changed |= drag("Vertical curve needed above a change of (per mille)", norms.curve_threshold, 0.1f, "%.2f");
changed |= ImGui::InputDouble("Constant grade between curves at least (m)", &norms.element_min, 0.0, 0.0, "%.0f"); changed |= drag("Constant grade between curves at least (m)", norms.element_min, 1.0f, "%.0f");
changed |= ImGui::InputDouble("Tolerance at fixed points (m)", &norms.fixed_tolerance, 0.0, 0.0, "%.3f"); changed |= drag("Tolerance at fixed points (m)", norms.fixed_tolerance, 0.001f, "%.3f");
ImGui::PopItemWidth(); ImGui::PopItemWidth();
if (ImGui::SmallButton("Automatic R at all points")) if (ImGui::SmallButton(STR_C("Automatic R at all points")))
{ {
for (auto &point : line.points) for (auto &point : line.points)
point.automatic = true; point.automatic = true;
changed = true; changed = true;
} }
ImGui::TextDisabled("Required R for %.0f km/h: %.0f m", line.speed, profile::required_radius(line)); ImGui::TextDisabled(STR_C("Required R for %.0f km/h: %.0f m"), line.speed, profile::required_radius(line));
ImGui::TreePop(); ImGui::TreePop();
} }
if (state.context.start_joined)
changed |= ImGui::Checkbox("Join the adjoining track at the start", &line.join_start); ImGui::Separator();
if (state.context.start_joined && state.context.end_joined) auto const errors{std::count_if(state.issues.begin(), state.issues.end(), [](profile::issue const &Issue) { return Issue.error; })};
ImGui::SameLine(); auto const warnings{static_cast<std::ptrdiff_t>(state.issues.size()) - errors};
if (state.context.end_joined) if (errors > 0)
changed |= ImGui::Checkbox("Join the adjoining track at the end", &line.join_end); ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), STR_C("%d errors, %d warnings: see the Remarks in the strip"), static_cast<int>(errors), static_cast<int>(warnings));
if ((state.context.start_joined || state.context.end_joined) && ImGui::IsItemHovered()) else if (warnings > 0)
ImGui::SetTooltip("Keeps the elevation of the joint and takes over the grade of the adjoining track,\n" ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), STR_C("%d warnings: see the Remarks in the strip"), static_cast<int>(warnings));
"with a vertical curve which starts right at the joint"); else
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "%s", STR_C("The grade line meets the limits"));
ImGui::TextDisabled(STR_C("Largest departure from the track: %.3f m"), state.departure);
if (ImGui::Button(STR_C("Recognize from the track")))
{
profile_recognize();
state.changed = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Sets the points of the grade line from the existing track"));
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.68f, 0.3f, 1.0f));
if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 0.0f)))
profile_apply();
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Sets the heights of the track along the route to the grade line, Ctrl+Z takes it back"));
return changed; return changed;
} }
void editor_mode::render_profile_issues() void editor_mode::render_profile_issues()
{ {
auto &state{m_profile}; auto &state{m_profile};
ImGui::Text("Largest departure from the track: %.3f m", state.departure);
ImGui::BeginChild("##profileissues", ImVec2(0.0f, 0.0f), false); ImGui::BeginChild("##profileissues", ImVec2(0.0f, 0.0f), false);
if (state.issues.empty()) if (state.issues.empty())
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "The grade line meets the limits"); ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "%s", STR_C("The grade line meets the limits"));
else
ImGui::TextDisabled("%s", STR_C("LMB on a remark shows its place"));
for (std::size_t i = 0; i < state.issues.size(); ++i) for (std::size_t i = 0; i < state.issues.size(); ++i)
{ {
auto const &issue{state.issues[i]}; auto const &issue{state.issues[i]};
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
ImGui::PushStyleColor(ImGuiCol_Text, issue.error ? ImVec4(1.0f, 0.4f, 0.3f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_Text, issue.error ? ImVec4(1.0f, 0.4f, 0.3f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f));
if (ImGui::Selectable(issue.text.c_str())) auto const text{format("%s km %s %s", issue.error ? "x" : "!", profile::format_chainage(issue.chainage + state.origin).c_str(), issue.text.c_str())};
if (ImGui::Selectable(text.c_str()))
{ {
if (issue.point >= 0) if (issue.point >= 0)
state.selected = issue.point; state.selected = issue.point;
@@ -712,21 +798,21 @@ void editor_mode::render_profile_point()
auto const count{static_cast<int>(points.size())}; auto const count{static_cast<int>(points.size())};
if (state.selected >= count) if (state.selected >= count)
state.selected = -1; state.selected = -1;
auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); };
auto const kind_of = [](double const Change) { return Change > 0.0 ? STR_C("sag") : STR_C("crest"); };
if (state.selected < 0) if (state.selected < 0)
{ {
ImGui::TextDisabled("LMB: select and drag a point (Ctrl: height only, Shift: keep the grade before)\n" ImGui::TextWrapped("%s", STR_C("No point selected. Click a point of the grade line in the plot, or double click the line to add one."));
"Green grip on a curve: drag to set R Double click on the line: insert a point\n"
"Del: remove the point Wheel: zoom Ctrl+wheel: exaggeration RMB/MMB: pan");
} }
else if (points[state.selected].joint) else if (points[state.selected].joint)
{ {
auto const index{state.selected}; auto const index{state.selected};
auto const &point{points[index]}; auto const &point{points[index]};
auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0};
ImGui::Text("Point %d: joint with the adjoining track (placed automatically)", index + 1); ImGui::Text(STR_C("Point %d: joint with the adjoining track"), index + 1);
ImGui::Text("km %s H %.3f R %.0f", profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius); ImGui::Text(STR_C("km %s H %.3f m R %.0f m"), profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius);
ImGui::Text("Grade %+.3f -> %+.3f per mille (%s)", profile::grade_after(state.line, index - 1) * 1000.0, profile::grade_after(state.line, index) * 1000.0, change > 0.0 ? "sag" : "crest"); ImGui::Text(STR_C("Grade %+.2f -> %+.2f per mille (%s)"), profile::grade_after(state.line, index - 1) * 1000.0, profile::grade_after(state.line, index) * 1000.0, kind_of(change));
ImGui::TextDisabled("Moves with the neighbouring point; untick the joining to edit the end freely"); ImGui::TextWrapped("%s", STR_C("Placed automatically, moves with the neighbouring point; untick the joining in the inspector to edit the end freely"));
} }
else else
{ {
@@ -734,21 +820,24 @@ void editor_mode::render_profile_point()
auto &point{points[index]}; auto &point{points[index]};
bool const end{index == 0 || index == count - 1}; bool const end{index == 0 || index == count - 1};
bool const locked{end_locked(state.line, state.context, index)}; bool const locked{end_locked(state.line, state.context, index)};
ImGui::Text("Point %d%s", index + 1, locked ? " (end, joined to the adjoining track)" : end ? " (end of the route)" : ""); ImGui::Text(STR_C("Point %d"), index + 1);
ImGui::PushItemWidth(110.0f); ImGui::SameLine();
ImGui::TextDisabled(STR_C("km %s%s"), profile::format_chainage(point.chainage + state.origin).c_str(), locked ? STR_C(", joined to the adjoining track") : end ? STR_C(", end of the route") : "");
ImGui::PushItemWidth(std::max(90.0f, ImGui::GetContentRegionAvail().x * 0.4f));
auto chainage{point.chainage + state.origin}; auto chainage{point.chainage + state.origin};
if (ImGui::InputDouble("Chainage (m)", &chainage, 0.0, 0.0, "%.2f") && false == end) if (false == end && drag("Chainage (m)", chainage, 0.5f, "%.2f"))
{ {
point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5);
changed = true; changed = true;
} }
ImGui::SameLine(); if (locked)
ImGui::TextDisabled("km %s", profile::format_chainage(point.chainage + state.origin).c_str()); ImGui::Text(STR_C("Elevation %.3f m"), point.elevation);
changed |= ImGui::InputDouble("Elevation (m)", &point.elevation, 0.0, 0.0, "%.3f", locked ? ImGuiInputTextFlags_ReadOnly : 0); else
changed |= drag("Elevation (m)", point.elevation, 0.01f, "%.3f");
if (index > 0 && false == locked) if (index > 0 && false == locked)
{ {
auto grade{profile::grade_after(state.line, index - 1) * 1000.0}; auto grade{profile::grade_after(state.line, index - 1) * 1000.0};
if (ImGui::InputDouble("Grade from the previous (per mille)", &grade, 0.0, 0.0, "%.3f")) if (drag("Grade before (per mille)", grade, 0.05f, "%+.2f"))
{ {
point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage); point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage);
changed = true; changed = true;
@@ -757,7 +846,7 @@ void editor_mode::render_profile_point()
if (index + 1 < count) if (index + 1 < count)
{ {
auto grade{profile::grade_after(state.line, index) * 1000.0}; auto grade{profile::grade_after(state.line, index) * 1000.0};
if (ImGui::InputDouble("Grade to the next (per mille)", &grade, 0.0, 0.0, "%.3f")) if (drag("Grade after (per mille)", grade, 0.05f, "%+.2f"))
{ {
points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage); points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage);
changed = true; changed = true;
@@ -765,24 +854,24 @@ void editor_mode::render_profile_point()
} }
if (false == end) if (false == end)
{ {
if (ImGui::InputDouble("Vertical curve R (m, 0: none)", &point.radius, 0.0, 0.0, "%.0f")) if (drag("Vertical curve R (m)", point.radius, 10.0f, "%.0f"))
{ {
point.automatic = false; point.automatic = false;
changed = true; changed = true;
} }
point.radius = std::max(0.0, point.radius); point.radius = std::max(0.0, point.radius);
ImGui::SameLine(); ImGui::SameLine();
changed |= ImGui::Checkbox("Auto", &point.automatic); changed |= ImGui::Checkbox(STR_C("auto"), &point.automatic);
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("R follows the limits: the required radius where the change of grade needs a curve"); ImGui::SetTooltip("%s", STR_C("R follows the limits: the required radius where the change of grade needs a curve; 0: no curve"));
auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0};
auto const tangent{point.radius * std::abs(change) / 2000.0}; auto const tangent{point.radius * std::abs(change) / 2000.0};
ImGui::Text("Change of grade %+.3f per mille, %s", change, change > 0.0 ? "sag" : "crest"); ImGui::TextDisabled(STR_C("Change of grade %+.2f per mille, %s"), change, kind_of(change));
if (point.radius > 0.0) if (point.radius > 0.0)
ImGui::Text("Tangent T %.2f m, sagitta f %.3f m", tangent, tangent * tangent / (2.0 * point.radius)); ImGui::TextDisabled(STR_C("Tangent T %.2f m, sagitta f %.3f m"), tangent, tangent * tangent / (2.0 * point.radius));
} }
ImGui::PopItemWidth(); ImGui::PopItemWidth();
if (index + 1 < count && ImGui::SmallButton("Insert a point halfway to the next")) if (index + 1 < count && ImGui::Button(STR_C("Add a point after")))
{ {
profile::pvi inserted; profile::pvi inserted;
inserted.chainage = (point.chainage + points[index + 1].chainage) * 0.5; inserted.chainage = (point.chainage + points[index + 1].chainage) * 0.5;
@@ -794,7 +883,7 @@ void editor_mode::render_profile_point()
if (false == end) if (false == end)
{ {
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton("Remove the point")) if (ImGui::Button(STR_C("Remove Del")))
{ {
points.erase(points.begin() + index); points.erase(points.begin() + index);
state.selected = -1; state.selected = -1;
@@ -802,17 +891,46 @@ void editor_mode::render_profile_point()
} }
} }
} }
ImGui::BeginChild("##profilepoints", ImVec2(0.0f, 0.0f), true); if (changed)
profile_edited();
}
void editor_mode::render_profile_points()
{
auto &state{m_profile};
auto const &points{state.line.points};
ImGui::Columns(4, "##profilepointcolumns", false);
for (auto const *header : {"#", "km", "H (m)", "R (m)"})
{
ImGui::TextDisabled("%s", header);
ImGui::NextColumn();
}
ImGui::Separator();
for (int i = 0; i < static_cast<int>(points.size()); ++i) for (int i = 0; i < static_cast<int>(points.size()); ++i)
{ {
auto const &point{points[i]}; auto const &point{points[i]};
auto const label{format("%2d km %s H %.3f %s##point%d", i + 1, profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius > 0.0 ? format("R %.0f%s", point.radius, point.joint ? " joint" : point.automatic ? " auto" : "").c_str() : "", i)}; if (ImGui::Selectable(format("%d##point%d", i + 1, i).c_str(), state.selected == i, ImGuiSelectableFlags_SpanAllColumns))
if (ImGui::Selectable(label.c_str(), state.selected == i)) {
state.selected = (state.selected == i ? -1 : i); state.selected = i;
auto const half{(state.view_to - state.view_from) * 0.5};
if (point.chainage < state.view_from || point.chainage > state.view_to)
{
state.view_from = point.chainage - half;
state.view_to = point.chainage + half;
}
}
ImGui::NextColumn();
ImGui::TextUnformatted(profile::format_chainage(point.chainage + state.origin).c_str());
ImGui::NextColumn();
ImGui::Text("%.3f", point.elevation);
ImGui::NextColumn();
if (point.radius > 0.0)
ImGui::Text("%.0f%s", point.radius, point.joint ? STR_C(" joint") : point.automatic ? STR_C(" auto") : "");
else
ImGui::TextDisabled("-");
ImGui::NextColumn();
} }
ImGui::EndChild(); ImGui::Columns(1);
if (changed)
profile_edited();
} }
// screen mapping of the profile canvas, chainage across and elevation upwards // screen mapping of the profile canvas, chainage across and elevation upwards
@@ -849,11 +967,50 @@ struct editor_mode::profile_view
} }
}; };
void editor_mode::render_profile_canvas() void editor_mode::render_profile_toolbar()
{
auto &state{m_profile};
auto *draw{ImGui::GetWindowDrawList()};
auto const swatch = [&](ImU32 const Colour) {
auto const at{ImGui::GetCursorScreenPos()};
auto const height{ImGui::GetFrameHeight()};
draw->AddLine(ImVec2(at.x, at.y + height * 0.5f), ImVec2(at.x + 16.0f, at.y + height * 0.5f), Colour, 3.0f);
ImGui::Dummy(ImVec2(16.0f, height));
ImGui::SameLine(0.0f, 4.0f);
};
auto const legend = [&](ImU32 const Colour, char const *Label) {
swatch(Colour);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(STR_C(Label));
ImGui::SameLine(0.0f, 14.0f);
};
auto const toggle = [&](ImU32 const Colour, char const *Label, bool &Value) {
swatch(Colour);
ImGui::Checkbox(STR_C(Label), &Value);
ImGui::SameLine(0.0f, 14.0f);
};
legend(kGradeLine, "Grade line");
legend(kTangents, "Tangents");
toggle(kTrack, STR_C("Track"), state.show_track);
toggle(kTerrain, STR_C("Terrain"), state.show_terrain);
toggle(kPlan, STR_C("Plan curvature"), state.show_plan);
legend(kSwitchLabel, "Switches");
ImGui::SetNextItemWidth(130.0f);
ImGui::SliderFloat("##exaggeration", &state.exaggeration, 1.0f, 500.0f, STR_C("heights x%.0f"), 2.0f);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Exaggeration of the heights against the lengths, also Ctrl+wheel over the plot"));
ImGui::SameLine();
if (ImGui::Button(STR_C("Fit")))
profile_fit_view();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Shows the whole route"));
}
void editor_mode::render_profile_canvas(float const Height)
{ {
auto &state{m_profile}; auto &state{m_profile};
auto const available{ImGui::GetContentRegionAvail()}; auto const available{ImGui::GetContentRegionAvail()};
ImVec2 const size{std::max(200.0f, available.x), std::max(240.0f, available.y * 0.6f)}; ImVec2 const size{std::max(200.0f, available.x), std::max(100.0f, Height)};
ImVec2 const corner{ImGui::GetCursorScreenPos()}; ImVec2 const corner{ImGui::GetCursorScreenPos()};
ImGui::InvisibleButton("##profilecanvas", size); ImGui::InvisibleButton("##profilecanvas", size);
bool const hovered{ImGui::IsItemHovered()}; bool const hovered{ImGui::IsItemHovered()};
@@ -874,9 +1031,47 @@ void editor_mode::render_profile_canvas()
{ {
auto const x{view.x_of(state.hover)}; auto const x{view.x_of(state.hover)};
draw->AddLine(ImVec2(x, view.plot_top()), ImVec2(x, view.bottom_right().y), IM_COL32(255, 255, 255, 90)); draw->AddLine(ImVec2(x, view.plot_top()), ImVec2(x, view.bottom_right().y), IM_COL32(255, 255, 255, 90));
profile_canvas_tooltip(); if (state.hover_point < 0 && state.hover_grip < 0 && state.curve_grip < 0)
profile_canvas_tooltip();
}
// what the mouse does over the thing under it
char const *hint{"wheel: zoom Ctrl+wheel: heights RMB/MMB drag: pan double click on the line: add a point"};
if (state.dragging >= 0)
hint = "Ctrl: only the height Shift: keep the grade before";
else if (state.curve_grip >= 0 || state.hover_grip >= 0)
hint = "drag up or down: radius of the vertical curve";
else if (state.hover_point >= 0)
hint = "drag: move the point Ctrl+drag: only the height Shift+drag: keep the grade before Del: remove";
else if (state.hover_line)
hint = "double click: add a point here";
if (hovered || state.dragging >= 0 || state.curve_grip >= 0)
{
auto const text{std::string{STR_C(hint)}};
auto const size{ImGui::CalcTextSize(text.c_str())};
plot_label(*draw, ImVec2(view.bottom_right().x - size.x - 8.0f, view.plot_bottom() - size.y - 6.0f), IM_COL32(255, 255, 255, 230), text);
} }
draw->PopClipRect(); draw->PopClipRect();
auto const &points{state.line.points};
if (state.dragging >= 0 && state.dragging < static_cast<int>(points.size()))
{
auto const index{state.dragging};
ImGui::BeginTooltip();
ImGui::Text(STR_C("Point %d"), index + 1);
ImGui::Text(STR_C("km %s H %.3f m"), profile::format_chainage(points[index].chainage + state.origin).c_str(), points[index].elevation);
if (index > 0)
ImGui::Text(STR_C("before %+.2f per mille"), profile::grade_after(state.line, index - 1) * 1000.0);
if (index + 1 < static_cast<int>(points.size()))
ImGui::Text(STR_C("after %+.2f per mille"), profile::grade_after(state.line, index) * 1000.0);
ImGui::EndTooltip();
}
else if (state.curve_grip > 0 && state.curve_grip < static_cast<int>(points.size()))
{
ImGui::BeginTooltip();
ImGui::Text("R %.0f m", points[state.curve_grip].radius);
ImGui::EndTooltip();
}
} }
void editor_mode::profile_fit_exaggeration(profile_view const &View) void editor_mode::profile_fit_exaggeration(profile_view const &View)
@@ -966,6 +1161,21 @@ bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hover
return -1; return -1;
}; };
bool changed{false}; bool changed{false};
bool const busy{state.dragging >= 0 || state.curve_grip >= 0 || state.panning};
state.hover_point = Hovered && false == busy ? hit_point() : -1;
state.hover_grip = Hovered && false == busy && state.hover_point < 0 ? hit_grip() : -1;
state.hover_line = false;
if (Hovered && false == busy && state.hover_point < 0 && state.hover_grip < 0 && points.size() >= 2)
{
auto const chainage{View.chainage_of(mouse.x)};
state.hover_line = chainage > points.front().chainage && chainage < points.back().chainage && std::abs(View.y_of(profile::elevation(line, chainage)) - mouse.y) < 12.0f;
}
if (state.dragging >= 0 || state.panning)
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll);
else if (state.curve_grip >= 0 || state.hover_grip >= 0)
ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
else if (state.hover_point >= 0 || state.hover_line)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (Hovered && ImGui::IsMouseClicked(0)) if (Hovered && ImGui::IsMouseClicked(0))
{ {
auto const point{hit_point()}; auto const point{hit_point()};
@@ -1054,19 +1264,19 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw
{ {
auto const x{View.x_of(s)}; auto const x{View.x_of(s)};
Draw.AddLine(ImVec2(x, plottop), ImVec2(x, plotbottom), kGrid); Draw.AddLine(ImVec2(x, plottop), ImVec2(x, plotbottom), kGrid);
Draw.AddText(ImVec2(x + 2.0f, View.corner.y + 2.0f), kLabel, profile::format_chainage(s + state.origin).c_str()); Draw.AddText(ImVec2(x + 3.0f, View.corner.y + 2.0f), kLabel, profile::format_chainage(s + state.origin).c_str());
} }
auto const heightstep{nice_step(View.vertical * 45.0)}; auto const heightstep{nice_step(View.vertical * 45.0)};
for (auto h = std::floor(View.height_of(plotbottom) / heightstep) * heightstep; h <= View.height_of(plottop); h += heightstep) for (auto h = std::floor(View.height_of(plotbottom) / heightstep) * heightstep; h <= View.height_of(plottop); h += heightstep)
{ {
auto const y{View.y_of(h)}; auto const y{View.y_of(h)};
Draw.AddLine(ImVec2(View.corner.x, y), ImVec2(bottomright.x, y), kGrid); Draw.AddLine(ImVec2(View.corner.x, y), ImVec2(bottomright.x, y), kGrid);
Draw.AddText(ImVec2(View.corner.x + 2.0f, y - 14.0f), kLabel, format("%.1f", h).c_str());
} }
for (auto const &zone : state.context.switches) for (auto const &zone : state.context.switches)
{ {
Draw.AddRectFilled(ImVec2(View.x_of(zone.from), plottop), ImVec2(std::max(View.x_of(zone.to), View.x_of(zone.from) + 2.0f), plotbottom), kSwitch); Draw.AddRectFilled(ImVec2(View.x_of(zone.from), plottop), ImVec2(std::max(View.x_of(zone.to), View.x_of(zone.from) + 2.0f), plotbottom), kSwitch);
Draw.AddText(ImVec2(View.x_of(zone.from) + 2.0f, plottop + 2.0f), IM_COL32(200, 160, 255, 220), zone.name.c_str()); if (View.x_of(zone.to) - View.x_of(zone.from) > 20.0f || View.across < 0.5)
Draw.AddText(ImVec2(View.x_of(zone.from) + 2.0f, plotbottom - 16.0f), kSwitchLabel, zone.name.c_str());
} }
auto const polyline = [&](auto const &Height, ImU32 const Colour, float const Thickness) { auto const polyline = [&](auto const &Height, ImU32 const Colour, float const Thickness) {
@@ -1093,7 +1303,31 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw
} }
}; };
if (state.show_terrain) if (state.show_terrain)
{
auto const fill{(kTerrain & 0x00FFFFFFu) | 0x30000000u};
float lastx{-1e9f};
ImVec2 previous{};
bool open{false};
for (std::size_t i = 0; i < samples.size(); ++i)
{
auto const x{View.x_of(samples[i].chainage)};
auto const h{state.terrain[i]};
if (std::isnan(h) || x < View.corner.x - 50.0f || x > bottomright.x + 50.0f)
{
open = false;
continue;
}
if (open && x - lastx < 2.0f && i + 1 < samples.size())
continue;
ImVec2 const next{x, std::min(View.y_of(h), plotbottom)};
if (open)
Draw.AddQuadFilled(previous, next, ImVec2(next.x, plotbottom), ImVec2(previous.x, plotbottom), fill);
previous = next;
lastx = x;
open = true;
}
polyline([&](std::size_t const I) { return state.terrain[I]; }, kTerrain, 1.5f); polyline([&](std::size_t const I) { return state.terrain[I]; }, kTerrain, 1.5f);
}
if (state.show_track) if (state.show_track)
polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f); polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f);
@@ -1117,8 +1351,11 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw
auto const x{View.x_of(middle)}; auto const x{View.x_of(middle)};
if (View.x_of(points[i + 1].chainage) - View.x_of(points[i].chainage) < 70.0f) if (View.x_of(points[i + 1].chainage) - View.x_of(points[i].chainage) < 70.0f)
continue; continue;
auto const text{format("%+.2f o/oo L %.1f", profile::grade_after(line, i) * 1000.0, points[i + 1].chainage - points[i].chainage)}; auto const text{format("%+.2f o/oo L %.0f m", profile::grade_after(line, i) * 1000.0, points[i + 1].chainage - points[i].chainage)};
Draw.AddText(ImVec2(x - 40.0f, View.y_of(profile::elevation(line, middle)) - 30.0f), kGradeLine, text.c_str()); auto const size{ImGui::CalcTextSize(text.c_str())};
if (View.x_of(points[i + 1].chainage) - View.x_of(points[i].chainage) < size.x + 30.0f)
continue;
plot_label(Draw, ImVec2(x - size.x * 0.5f, View.y_of(profile::elevation(line, middle)) - size.y - 10.0f), kGradeLine, text);
} }
for (auto const &c : profile::curves(line)) for (auto const &c : profile::curves(line))
{ {
@@ -1129,24 +1366,34 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw
} }
auto const chainage{points[c.point].chainage}; auto const chainage{points[c.point].chainage};
ImVec2 const grip{View.x_of(chainage), View.y_of(profile::elevation(line, chainage))}; ImVec2 const grip{View.x_of(chainage), View.y_of(profile::elevation(line, chainage))};
Draw.AddCircleFilled(grip, 4.5f, overlay_color::grip, 4); bool const lit{static_cast<int>(c.point) == state.curve_grip || static_cast<int>(c.point) == state.hover_grip};
if (static_cast<int>(c.point) == state.curve_grip) Draw.AddCircleFilled(grip, lit ? 6.5f : 4.5f, overlay_color::grip, 4);
Draw.AddCircle(grip, 9.0f, overlay_color::highlight, 12, 2.0f); if (lit)
Draw.AddText(ImVec2(grip.x + 7.0f, grip.y + 4.0f), overlay_color::grip, format("R %.0f", points[c.point].radius).c_str()); Draw.AddCircle(grip, 10.0f, overlay_color::highlight, 12, 2.0f);
plot_label(Draw, ImVec2(grip.x + 8.0f, grip.y + 6.0f), overlay_color::grip, format("R %.0f", points[c.point].radius));
} }
for (int i = 0; i < static_cast<int>(points.size()); ++i) for (int i = 0; i < static_cast<int>(points.size()); ++i)
{ {
ImVec2 const at{View.x_of(points[i].chainage), View.y_of(points[i].elevation)}; ImVec2 const at{View.x_of(points[i].chainage), View.y_of(points[i].elevation)};
bool const lit{i == state.hover_point || i == state.dragging};
auto const radius{lit ? 7.0f : 5.5f};
if (i == 0 || i + 1 == static_cast<int>(points.size())) if (i == 0 || i + 1 == static_cast<int>(points.size()))
Draw.AddRectFilled(ImVec2(at.x - 5.0f, at.y - 5.0f), ImVec2(at.x + 5.0f, at.y + 5.0f), IM_COL32(70, 140, 255, 255)); Draw.AddRectFilled(ImVec2(at.x - radius, at.y - radius), ImVec2(at.x + radius, at.y + radius), IM_COL32(70, 140, 255, 255));
else if (points[i].joint) else if (points[i].joint)
Draw.AddQuadFilled(ImVec2(at.x, at.y - 6.0f), ImVec2(at.x + 6.0f, at.y), ImVec2(at.x, at.y + 6.0f), ImVec2(at.x - 6.0f, at.y), IM_COL32(90, 210, 230, 255)); Draw.AddQuadFilled(ImVec2(at.x, at.y - radius), ImVec2(at.x + radius, at.y), ImVec2(at.x, at.y + radius), ImVec2(at.x - radius, at.y), IM_COL32(90, 210, 230, 255));
else else
Draw.AddCircleFilled(at, 5.5f, overlay_color::marked); Draw.AddCircleFilled(at, radius, overlay_color::marked);
if (i == state.selected) if (i == state.selected)
Draw.AddCircle(at, 10.0f, overlay_color::highlight, 16, 2.0f); Draw.AddCircle(at, 11.0f, overlay_color::highlight, 16, 2.5f);
Draw.AddText(ImVec2(at.x + 7.0f, at.y - 18.0f), overlay_color::marked, format("%d %.3f", i + 1, points[i].elevation).c_str()); else if (lit)
Draw.AddCircle(at, 11.0f, IM_COL32(255, 255, 255, 160), 16, 1.5f);
// the heights only where they're asked for, the numbers of the points always
auto const text{i == state.selected || lit ? format("%d H %.3f", i + 1, points[i].elevation) : std::to_string(i + 1)};
plot_label(Draw, ImVec2(at.x + 8.0f, at.y - 22.0f), overlay_color::marked, text);
} }
// heights along the left edge, over everything else
for (auto h = std::floor(View.height_of(plotbottom) / heightstep) * heightstep; h <= View.height_of(plottop); h += heightstep)
plot_label(Draw, ImVec2(View.corner.x + 4.0f, View.y_of(h) - 8.0f), kLabel, format("%.1f", h));
for (auto const &issue : state.issues) for (auto const &issue : state.issues)
{ {
auto const x{View.x_of(issue.chainage)}; auto const x{View.x_of(issue.chainage)};
@@ -1172,7 +1419,7 @@ void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw
lastx = x; lastx = x;
open = true; open = true;
} }
Draw.AddText(ImVec2(View.corner.x + 2.0f, plotbottom + 1.0f), kLabel, "plan: curvature (up: left)"); Draw.AddText(ImVec2(View.corner.x + 4.0f, plotbottom + 1.0f), kLabel, STR_C("plan: curvature (up: left)"));
} }
} }
@@ -1183,15 +1430,15 @@ void editor_mode::profile_canvas_tooltip() const
auto const designed{profile::elevation(state.line, state.hover)}; auto const designed{profile::elevation(state.line, state.hover)};
auto const existing{editor_track::sampled_elevation(samples, state.hover)}; auto const existing{editor_track::sampled_elevation(samples, state.hover)};
ImGui::BeginTooltip(); ImGui::BeginTooltip();
ImGui::Text("km %s", profile::format_chainage(state.hover + state.origin).c_str()); ImGui::Text(STR_C("km %s"), profile::format_chainage(state.hover + state.origin).c_str());
ImGui::Text("Grade line %.3f m, %+.2f per mille", designed, profile::grade(state.line, state.hover) * 1000.0); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kGradeLine), STR_C("Grade line %.3f m, %+.2f per mille"), designed, profile::grade(state.line, state.hover) * 1000.0);
ImGui::Text("Track %.3f m (%+.3f), %+.2f per mille", existing, existing - designed, editor_track::sampled_grade(samples, state.hover) * 1000.0); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kTrack), STR_C("Track %.3f m (%+.3f), %+.2f per mille"), existing, existing - designed, editor_track::sampled_grade(samples, state.hover) * 1000.0);
auto const next{std::lower_bound(samples.begin(), samples.end(), state.hover, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; auto const next{std::lower_bound(samples.begin(), samples.end(), state.hover, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })};
auto const index{static_cast<std::size_t>(std::distance(samples.begin(), std::min(next, std::prev(samples.end()))))}; auto const index{static_cast<std::size_t>(std::distance(samples.begin(), std::min(next, std::prev(samples.end()))))};
if (index < state.terrain.size() && false == std::isnan(state.terrain[index])) if (index < state.terrain.size() && false == std::isnan(state.terrain[index]))
ImGui::Text("Terrain %.3f m, %s %.2f m", state.terrain[index], designed > state.terrain[index] ? "fill" : "cut", std::abs(designed - state.terrain[index])); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kTerrain), STR_C("Terrain %.3f m, %s %.2f m"), state.terrain[index], designed > state.terrain[index] ? STR_C("fill") : STR_C("cut"), std::abs(designed - state.terrain[index]));
auto const &span{state.route.spans[samples[index].span]}; auto const &span{state.route.spans[samples[index].span]};
ImGui::TextDisabled("%s%s", span.track->name().c_str(), span.track->eType != tt_Normal ? " (switch, tilted as a whole with its branch)" : ""); ImGui::TextDisabled("%s%s", span.track->name().c_str(), span.track->eType != tt_Normal ? STR_C(" (switch, tilted as a whole with its branch)") : "");
ImGui::EndTooltip(); ImGui::EndTooltip();
} }

View File

@@ -220,7 +220,8 @@ void editor_mode::render_road_menu()
{ {
if (ImGui::BeginMenu("Roads")) if (ImGui::BeginMenu("Roads"))
{ {
ImGui::MenuItem("Road editor", nullptr, &m_roadtool.window); if (ImGui::MenuItem("Road editor", nullptr, &m_roadtool.window) && m_roadtool.window)
m_track_window_open = false;
ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes);
ImGui::EndMenu(); ImGui::EndMenu();
} }

View File

@@ -0,0 +1,382 @@
/*
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 "application/editormode.h"
#include "application/editoruilayer.h"
#include "application/editorprojection.h"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "scene/scenelayers.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "world/Track.h"
#include "imgui/imgui.h"
#include "utilities/translation.h"
#include <algorithm>
#include <chrono>
#include <cmath>
namespace
{
double const kSpeedLineReach{5000.0}; // m each way, for the line through the selected path
double const kSpeedViewDistance{25.0};
double const kSpeedOverlayRange{3000.0};
bool rail(TTrack const &Track)
{
return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && Track.m_road == nullptr && editor_track::is_supported(Track);
}
std::string speed_text(double const Speed)
{
return Speed > 0.0 ? format("%.0f", Speed) : std::string{"none"};
}
} // namespace
void editor_mode::speed_start()
{
auto &state{m_speed};
state.queue.clear();
state.next = 0;
state.results.clear();
state.selected = -1;
state.status.clear();
state.options.norms = m_route.design.norms;
auto const add = [&](TTrack *Track) {
for (int i = 0; i < static_cast<int>(Track->m_paths.size()); ++i)
state.queue.emplace_back(Track, i);
};
if (state.scope == 0)
{
auto *track{selected_track()};
if (track == nullptr || false == rail(*track))
{
state.status = STR_C("Select a track first");
return;
}
editor_track::chain line;
std::string error;
if (track->eType == tt_Normal && editor_track::find_run(*track, kSpeedLineReach, line, error))
{
for (auto *member : line.tracks)
add(member);
// the switches at the ends of the line too
for (auto *end : {line.tracks.front(), line.tracks.back()})
for (auto *neighbour : editor_track::neighbours(*end))
if (neighbour->eType == tt_Switch && rail(*neighbour) && std::none_of(state.queue.begin(), state.queue.end(), [&](auto const &Entry) { return Entry.first == neighbour; }))
add(neighbour);
}
else
{
add(track);
}
}
else
{
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && rail(*track))
add(track);
}
state.history = m_history.size();
}
void editor_mode::speed_step()
{
auto &state{m_speed};
if (state.next >= state.queue.size())
return;
auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)};
while (state.next < state.queue.size() && std::chrono::steady_clock::now() < deadline)
{
auto const &entry{state.queue[state.next++]};
if (entry.first->m_editorremoved)
continue;
state.results.push_back(speed_check::check(*entry.first, entry.second, state.options));
}
if (state.next < state.queue.size())
return;
// the worst first: the violations, then the warnings, each by the largest excess of the speed set over the safe one
auto const excess = [&](speed_check::verdict const &Verdict) { return Verdict.set > 0.0 ? Verdict.set - Verdict.safe : state.options.top - Verdict.safe; };
auto const rank = [&](speed_check::verdict const &Verdict) { return Verdict.violated(state.options) ? 0 : Verdict.warned(state.options) ? 1 : 2; };
std::stable_sort(state.results.begin(), state.results.end(), [&](auto const &A, auto const &B) {
auto const a{rank(A)};
auto const b{rank(B)};
if (a != b)
return a < b;
return a < 2 ? excess(A) > excess(B) : A.safe < B.safe;
});
auto const count{std::count_if(state.results.begin(), state.results.end(), [&](auto const &Verdict) { return Verdict.violated(state.options); })};
auto const warnings{std::count_if(state.results.begin(), state.results.end(), [&](auto const &Verdict) { return Verdict.warned(state.options); })};
state.status = format(STR_C("%zu paths checked, %d with the speed above what the geometry allows, %d switch branch warnings"), state.results.size(), static_cast<int>(count), static_cast<int>(warnings));
WriteLog("Editor: speed check - " + state.status, logtype::generic);
}
std::vector<int> editor_mode::speed_listed() const
{
std::vector<int> listed;
for (int i = 0; i < static_cast<int>(m_speed.results.size()); ++i)
{
auto const &verdict{m_speed.results[i]};
if ((m_speed.limits == 1 && verdict.set <= 0.0) || (m_speed.limits == 2 && verdict.set > 0.0))
continue;
if (m_speed.show_all || verdict.violated(m_speed.options) || verdict.warned(m_speed.options))
listed.push_back(i);
}
return listed;
}
void editor_mode::speed_focus(int const Index)
{
auto &state{m_speed};
if (Index < 0 || Index >= static_cast<int>(state.results.size()))
return;
state.selected = Index;
auto const &verdict{state.results[Index]};
auto const &paths{simulation::Paths.sequence()};
if (std::find(paths.begin(), paths.end(), verdict.track) == paths.end() || verdict.track->m_editorremoved)
return;
m_node = verdict.track;
ui()->set_node(m_node);
auto const &path{verdict.track->m_paths[std::min<std::size_t>(verdict.path, verdict.track->m_paths.size() - 1)]};
geometry::bezier const curve{path};
glm::dvec3 look{curve.first(0.5)};
look.y = 0.0;
look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0);
if (glm::dot(Camera.Pos - verdict.position, look) > 0.0)
look = -look;
look = glm::normalize(look - glm::dvec3(0.0, 0.25, 0.0));
m_focus_start_pos = Camera.Pos;
m_focus_start_angle = Camera.Angle;
m_focus_target_pos = verdict.position - look * kSpeedViewDistance;
m_focus_target_angle = glm::vec3(static_cast<float>(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast<float>(std::atan2(-look.x, -look.z)), 0.0f);
m_focus_active = true;
m_focus_time = 0.0;
m_focus_duration = 0.6;
}
void editor_mode::speed_apply(std::vector<int> const &Indices, bool const Branches)
{
auto &state{m_speed};
struct change
{
TTrack *track;
double limits[2]; // km/h for the path, and the diverging track of a switch, -1: none
};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<change> changes;
std::size_t locked{0};
for (auto const index : Indices)
{
auto const &verdict{state.results[index]};
auto const safe{Branches ? verdict.safe : verdict.hard};
if (safe >= state.options.top || verdict.track->m_editorremoved)
continue;
if (false == scene::Layers.editable(verdict.track))
{
++locked;
continue;
}
auto existing{std::find_if(changes.begin(), changes.end(), [&](change const &Change) { return Change.track == verdict.track; })};
if (existing == changes.end())
{
changes.push_back({verdict.track, {speed_check::path_velocity(*verdict.track, 0), speed_check::path_velocity(*verdict.track, 1)}});
states.emplace_back(verdict.track, editor_track::capture(*verdict.track));
existing = std::prev(changes.end());
}
auto &limit{existing->limits[verdict.track->eType == tt_Switch ? std::clamp(verdict.path, 0, 1) : 0]};
auto const speed{speed_check::suggested(safe)};
limit = limit > 0.0 ? std::min(limit, speed) : speed;
}
auto const skipped{locked > 0 ? format(", %zu in the files which can't be changed are left as they are", locked) : std::string{}};
if (changes.empty())
{
if (locked > 0)
state.status = STR_C("Nothing changed") + skipped;
return;
}
push_track_snapshot(std::move(states));
for (auto const &entry : changes)
{
auto const velocity{entry.track->eType == tt_Switch ? speed_check::switch_velocity(entry.limits[0], entry.limits[1]) : entry.limits[0]};
editor_track::velocity(*entry.track, velocity);
entry.track->mark_dirty();
}
for (auto &verdict : state.results)
verdict.set = speed_check::path_velocity(*verdict.track, verdict.path);
state.history = m_history.size();
state.status = format(STR_C("Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back"), changes.size()) + skipped;
WriteLog("Editor: speed check - " + state.status, logtype::generic);
}
void editor_mode::render_speed_body()
{
auto &state{m_speed};
ImGui::TextDisabled(STR_C("Speed limits of the paths against the speed their geometry allows:\nradius and cant, cant ramps, curves without the transitions, switch branches"));
ImGui::RadioButton(STR_C("Line through the selected path"), &state.scope, 0);
ImGui::SameLine();
ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1);
bool const running{state.next < state.queue.size()};
if (running)
{
auto const progress{static_cast<float>(state.next) / static_cast<float>(state.queue.size())};
ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str());
ImGui::SameLine();
if (ImGui::SmallButton(STR_C("Cancel")))
state.queue.resize(state.next);
}
else if (ImGui::Button(STR_C("Check")))
{
speed_start();
}
if (ImGui::TreeNode(STR_C("Limits")))
{
auto &options{state.options};
ImGui::PushItemWidth(100.0f);
ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &options.norms.unbalanced, 0.0, 0.0, "%.2f");
ImGui::InputDouble(STR_C("Switch branches (m/s2)"), &options.branch_unbalanced, 0.0, 0.0, "%.2f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m"));
ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &options.norms.cant_rate, 0.0, 0.0, "%.0f");
ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &options.norms.ramp_factor, 0.0, 0.0, "%.1f");
ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &options.norms.jerk, 0.0, 0.0, "%.2f");
ImGui::InputDouble(STR_C("Reference length (m)"), &options.reference_length, 0.0, 0.0, "%.0f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions"));
ImGui::PopItemWidth();
options.norms.unbalanced = std::max(0.05, options.norms.unbalanced);
options.branch_unbalanced = std::max(0.05, options.branch_unbalanced);
options.norms.cant_rate = std::max(1.0, options.norms.cant_rate);
options.norms.ramp_factor = std::max(1.0, options.norms.ramp_factor);
options.norms.jerk = std::max(0.01, options.norms.jerk);
options.reference_length = std::clamp(options.reference_length, 2.0, 200.0);
ImGui::TextDisabled(STR_C("Change the limits, then Check again"));
ImGui::TreePop();
}
if (false == state.status.empty())
ImGui::TextWrapped("%s", state.status.c_str());
if (state.results.empty())
return;
if (state.history != m_history.size())
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check"));
ImGui::SetNextItemWidth(190.0f);
ImGui::Combo("##speedlimits", &state.limits, "All the paths\0With a speed limit set\0Without a speed limit\0");
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all"));
ImGui::SameLine();
ImGui::Checkbox(STR_C("Within their limits too"), &state.show_all);
auto const listed{speed_listed()};
std::vector<int> violated;
for (auto const index : listed)
if (state.results[index].violated(state.options))
violated.push_back(index);
if (false == violated.empty())
{
ImGui::SameLine();
if (ImGui::Button(format(STR_C("Set the safe speed on %zu paths"), violated.size()).c_str()))
speed_apply(violated, false);
if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Lowers the speed limit of each listed path to its safe speed, rounded down to 10 km/h (5 km/h below 40).\n"
"The limits of the switch branches are only warnings and aren't applied here, the button of a single path does.\n"
"A switch has one velocity for both its tracks: negative limits only the diverging one, the main one keeping no limit.\n"
"When the main track needs a limit of its own, the lower of the two applies to both"));
}
auto const namewidth{std::max(120.0f, ImGui::GetContentRegionAvail().x * 0.3f)};
ImGui::Columns(6, STR_C("speedcolumns"), false);
ImGui::SetColumnWidth(0, namewidth + 8.0f);
for (auto const *header : {"Path", "Set", "Safe", "Suggested", "Cause", "R / cant"})
{
ImGui::TextDisabled("%s", header);
ImGui::NextColumn();
}
ImGui::Columns(1);
bool const chosen{state.selected >= 0 && state.selected < static_cast<int>(state.results.size())};
auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 5.0f : 0.0f};
ImGui::BeginChild("speedresults", ImVec2(0.0f, -details), true);
ImGui::Columns(6, STR_C("speedrows"), false);
ImGui::SetColumnWidth(0, namewidth);
ImGuiListClipper clipper(static_cast<int>(listed.size()));
while (clipper.Step())
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
{
auto const index{listed[row]};
auto const &verdict{state.results[index]};
bool const violated{verdict.violated(state.options)};
bool const bad{violated || verdict.warned(state.options)};
auto const colour{violated ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.75f, 0.3f, 1.0f)};
auto name{verdict.track->name().empty() ? std::string{"(noname)"} : verdict.track->name()};
if (verdict.track->eType == tt_Switch)
name += verdict.path == 0 ? " (main)" : " (branch)";
if (bad)
ImGui::PushStyleColor(ImGuiCol_Text, colour);
if (ImGui::Selectable((name + "##speed" + std::to_string(index)).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns))
speed_focus(index);
if (ImGui::IsItemHovered())
{
if (bad)
ImGui::PopStyleColor();
ImGui::SetTooltip("%s", speed_check::explain(verdict, state.options).c_str());
if (bad)
ImGui::PushStyleColor(ImGuiCol_Text, colour);
}
ImGui::NextColumn();
ImGui::TextUnformatted(speed_text(verdict.set).c_str());
ImGui::NextColumn();
ImGui::TextUnformatted(verdict.safe >= state.options.top ? STR_C("any") : format("%.0f", verdict.safe).c_str());
ImGui::NextColumn();
ImGui::TextUnformatted(verdict.safe >= state.options.top ? "-" : format("%.0f", speed_check::suggested(verdict.safe)).c_str());
ImGui::NextColumn();
ImGui::TextUnformatted(speed_check::describe(verdict.why));
ImGui::NextColumn();
if (verdict.radius > 0.0)
ImGui::Text(STR_C("%.0f m / %.0f mm"), verdict.radius, verdict.cant);
else
ImGui::TextUnformatted("-");
ImGui::NextColumn();
if (bad)
ImGui::PopStyleColor();
}
ImGui::Columns(1);
ImGui::EndChild();
if (chosen)
{
auto const &verdict{state.results[state.selected]};
ImGui::TextWrapped("%s", speed_check::explain(verdict, state.options).c_str());
if ((verdict.violated(state.options) || verdict.warned(state.options)) && ImGui::Button(STR_C("Set the safe speed on this path")))
speed_apply({state.selected}, true);
}
}
void editor_mode::draw_speed_overlay() const
{
auto const &state{m_speed};
if (false == state.open || state.results.empty())
return;
screen_projection const projection;
auto *drawlist{ImGui::GetBackgroundDrawList()};
glm::dvec3 const camera{Global.pCamera.Pos};
for (int i = 0; i < static_cast<int>(state.results.size()); ++i)
{
auto const &verdict{state.results[i]};
bool const violated{verdict.violated(state.options)};
if ((false == violated && false == verdict.warned(state.options)) || glm::distance(verdict.position, camera) > kSpeedOverlayRange)
continue;
ImVec2 screen;
if (false == projection.project(verdict.position, screen))
continue;
auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : violated ? IM_COL32(255, 70, 60, 230) : IM_COL32(255, 180, 60, 200)};
drawlist->AddCircle(screen, 12.0f, colour, 16, 2.5f);
auto const text{format("%s > %.0f", speed_text(verdict.set).c_str(), verdict.safe)};
drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -85,22 +85,6 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode)
if (m_filloptions) if (m_filloptions)
m_filloptions(); m_filloptions();
break; break;
case nodebank_panel::TRACK:
if (m_gaugewindow != nullptr)
{
auto const open{*m_gaugewindow};
if (open)
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
if (ImGui::Button(STR_C("Structure gauge"), ImVec2(-1.0f, 0.0f)))
*m_gaugewindow = false == *m_gaugewindow;
if (open)
ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"));
}
if (m_trackoptions)
m_trackoptions();
break;
default: default:
break; break;
} }
@@ -218,7 +202,13 @@ std::string const *editor_ui::get_active_node_template(bool bypassRandom)
nodebank_panel::edit_mode editor_ui::mode() nodebank_panel::edit_mode editor_ui::mode()
{ {
return m_nodebankpanel.mode; return m_track ? nodebank_panel::TRACK : m_nodebankpanel.mode;
}
void editor_ui::set_mode(nodebank_panel::edit_mode const Mode)
{
m_nodebankpanel.requested_mode = Mode;
m_nodebankpanel.mode = Mode;
m_nodebankpanel.is_open = true;
} }
float editor_ui::getSpacing() float editor_ui::getSpacing()
{ {

View File

@@ -37,6 +37,7 @@ class editor_ui : public ui_layer
functions_panel::rotation_mode rot_mode(); functions_panel::rotation_mode rot_mode();
const std::string *get_active_node_template(bool bypassRandom = false); const std::string *get_active_node_template(bool bypassRandom = false);
nodebank_panel::edit_mode mode(); nodebank_panel::edit_mode mode();
void set_mode(nodebank_panel::edit_mode const Mode);
float getSpacing(); float getSpacing();
// node bank and model set access (set selection widgets, set contents) // node bank and model set access (set selection widgets, set contents)
nodebank_panel &nodebank() nodebank_panel &nodebank()
@@ -50,9 +51,10 @@ class editor_ui : public ui_layer
{ {
m_filloptions = std::move(Renderer); m_filloptions = std::move(Renderer);
} }
void set_track_options(std::function<void()> Renderer) // the open track window takes the mouse over, whichever toolset tab is chosen
void set_track(bool const Track)
{ {
m_trackoptions = std::move(Renderer); m_track = Track;
} }
// draws menus of the editor mode in the menu bar, past the ones of the user interface // draws menus of the editor mode in the menu bar, past the ones of the user interface
void set_menu_options(std::function<void()> Renderer) void set_menu_options(std::function<void()> Renderer)
@@ -64,11 +66,6 @@ class editor_ui : public ui_layer
{ {
m_gizmooptions = std::move(Renderer); m_gizmooptions = std::move(Renderer);
} }
// switch of the structure gauge window in the menu bar (the window lives in the editor mode)
void set_gauge_window(bool *Open)
{
m_gaugewindow = Open;
}
// actions of the file menu, performed by the editor mode // actions of the file menu, performed by the editor mode
void set_file_actions(std::function<void()> Save, std::function<void()> Export); void set_file_actions(std::function<void()> Save, std::function<void()> Export);
// shows outcome of an operation in the layers window // shows outcome of an operation in the layers window
@@ -91,9 +88,8 @@ class editor_ui : public ui_layer
bool m_insertrandom{false}; // insert mode picks a random template from m_insertset bool m_insertrandom{false}; // insert mode picks a random template from m_insertset
model_set_ref m_insertset; model_set_ref m_insertset;
std::function<void()> m_filloptions; std::function<void()> m_filloptions;
std::function<void()> m_trackoptions;
std::function<void()> m_menuoptions; std::function<void()> m_menuoptions;
std::function<void()> m_gizmooptions; std::function<void()> m_gizmooptions;
scene::basic_node *m_node{nullptr}; // currently bound scene node, if any scene::basic_node *m_node{nullptr}; // currently bound scene node, if any
bool *m_gaugewindow{nullptr}; bool m_track{false};
}; };

View File

@@ -542,12 +542,12 @@ void nodebank_panel::render()
} }
// edit modes as tabs, each with its own settings // edit modes as tabs, each with its own settings
std::pair<char const *, edit_mode> const modes[] = {{"Select", MODIFY}, {"Track", TRACK}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}}; std::pair<char const *, edit_mode> const modes[] = {{"Select", MODIFY}, {"Insert", ADD}, {"Brush", BRUSH}, {"Area fill", FILL}, {"Copy to bank", COPY}};
if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown)) if (ImGui::BeginTabBar("##editmodes", ImGuiTabBarFlags_FittingPolicyResizeDown))
{ {
for (auto const &tab : modes) for (auto const &tab : modes)
{ {
if (false == ImGui::BeginTabItem(tab.first)) if (false == ImGui::BeginTabItem(tab.first, nullptr, requested_mode == tab.second ? ImGuiTabItemFlags_SetSelected : 0))
{ {
continue; continue;
} }
@@ -559,6 +559,7 @@ void nodebank_panel::render()
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
ImGui::EndTabBar(); ImGui::EndTabBar();
requested_mode = -1;
} }
ImGui::Separator(); ImGui::Separator();

View File

@@ -126,6 +126,7 @@ class nodebank_panel : public ui_panel
TRACK TRACK
}; };
edit_mode mode = MODIFY; edit_mode mode = MODIFY;
int requested_mode{-1}; // tab brought forward on the next frame
nodebank_panel(std::string const &Name, bool const Isopen); nodebank_panel(std::string const &Name, bool const Isopen);
void nodebank_reload(); void nodebank_reload();