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:
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
382
application/editormodespeed.cpp
Normal file
382
application/editormodespeed.cpp
Normal 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
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user