diff --git a/CMakeLists.txt b/CMakeLists.txt index 09144647..e403fd2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,11 +175,17 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorRoad.cpp" "editor/editorAlignment.cpp" +"editor/editorProfile.cpp" +"editor/editorGauge.cpp" +"editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormoderoad.cpp" +"application/editormodeprofile.cpp" +"application/editormodegauge.cpp" +"application/editormodeinfra.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 32dd37cf..002afb93 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -476,6 +476,7 @@ editor_mode::editor_mode() { ui()->set_gizmo_options([this]() { render_gizmo_options(); }); 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]() { render_object_menu(); render_road_menu(); @@ -487,8 +488,14 @@ editor_mode::editor_mode() { m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) { gather_ground_triangles(Min, Max, true, 50.0f, Out, true); }); + editor_track::observe(this); } +editor_mode::~editor_mode() +{ + editor_track::observe(nullptr); +} + std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh) { std::vector heights; @@ -1091,6 +1098,9 @@ bool editor_mode::update() simulation::State.update_clocks(); simulation::Environment.update(); + // bindings are matched with the objects by their locations, before anything gets moved + if (false == m_bindings_loaded && simulation::is_ready) + infra_load(); auto const deltarealtime = Timer::GetDeltaRenderTime(); @@ -1243,7 +1253,13 @@ bool editor_mode::update() draw_build_overlay(); draw_track_hints(); render_track_window(); + draw_profile_overlay(); + render_profile_window(); + draw_infra_overlay(); + render_infra_window(); } + update_gauge(); + render_gauge_window(); // roads: trajectories of the lanes, and the tools of the road window update_road_tool(); @@ -1865,6 +1881,9 @@ void editor_mode::save() } } + if (m_profile.changed) + profile_store(); + infra_store(); auto const result = scene::Layers.save(rootstatements); if (result.success && false == rootstatements.empty()) { diff --git a/application/editormode.h b/application/editormode.h index 51ff130e..141ef58b 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -22,6 +22,9 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTrack.hpp" #include "editor/editorAlignment.hpp" #include "editor/editorRoad.hpp" +#include "editor/editorProfile.hpp" +#include "editor/editorInfra.hpp" +#include "editor/editorGauge.hpp" #include #include @@ -33,12 +36,23 @@ http://mozilla.org/MPL/2.0/. class TAnimModel; class TTrack; -class editor_mode : public application_mode +class editor_mode : public application_mode, private editor_track::observer { public: + // model entering the structure gauge, at its deepest place + struct gauge_hit + { + std::string model; + glm::dvec3 point{0.0}; + double depth{0.0}; // m + TTrack const *track{nullptr}; // nearest one, may be gone by now + int path{0}; + glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan + }; // constructors editor_mode(); + ~editor_mode() override; // methods // initializes internal data structures of the mode. returns: true on success, false otherwise bool init() override; @@ -108,7 +122,7 @@ class editor_mode : public application_mode std::vector created; std::vector removed; editor_road::record roads; // what a change of the roads comes to - + std::vector infra; // objects which followed the paths }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -293,7 +307,8 @@ class editor_mode : public application_mode void select_track(scene::basic_node *Node); void render_track_gizmo(); void commit_track_drag(bool const Force); - void push_track_snapshot(std::vector> States, std::vector Created = {}); + void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}); + void trim_history(); void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); editor_track::point_ref m_track_point; std::vector m_track_drag; @@ -306,6 +321,57 @@ class editor_mode : public application_mode std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; std::array, 3> m_track_materials{}; + std::array m_track_material_edited{}; // path whose texture name is being typed + std::string m_track_mode_notice; + // structure gauge along the line of the selected path, shown in the 3d view + struct gauge_state + { + bool open{false}; // window + bool enabled{true}; // tunnel along the selected path + std::vector profiles; + int profile{0}; // railway + int road{-1}; + TTrack const *track{nullptr}; // scanned + std::size_t history{0}; + bool pending{false}; // rescan once the edit ends + struct + { + std::vector surface; // triangles of the tunnel + std::vector edges; // lines + std::vector hits; + } line; // along the selected path + struct + { + bool scanned{false}; + std::size_t history{0}; + std::string profile; + std::vector hits; + } map; // whole scenery + struct + { + std::vector surface; + std::vector edges; + } spot; // tunnel around the hit the camera went to + int current{-1}; // hit the camera went to + bool published{false}; // overlay up to date + std::shared_ptr scan; // of the whole scenery, done in slices over the frames + } m_gauge; + void gauge_load(); + // the chosen railway and road outlines + gauge::choice gauge_choice() const; + std::vector const &gauge_hits() const; + void update_gauge(); + void scan_gauge(TTrack &Track); + void scan_gauge_map(); + void step_gauge_map(); + void finish_gauge_map(); + void gauge_publish(); + void gauge_focus(int Index); + void gauge_spot(gauge_hit const &Hit); + void render_gauge_window(); + void render_gauge_hits(); + bool render_gauge_choice(char const *Label, int &Index, bool Road); + bool render_gauge_outline(int Index); struct route_design { TTrack *from{nullptr}; @@ -319,7 +385,13 @@ class editor_mode : public application_mode int grip{-1}; }; void render_route_ui(); + void render_route_ends(); + bool render_route_parameters(); + bool render_route_vertices(); + bool render_route_vertex(int const Index); + void render_route_result(); void route_reset(); + void route_bind_ends(); bool route_from_curve(TTrack &Track); void route_update() { m_route.result = alignment::compute(m_route.design); } void route_apply(); @@ -472,17 +544,156 @@ class editor_mode : public application_mode enum class track_tab { straights, route, path, turnout }; bool m_track_window_open{false}; void render_track_window(); + void render_track_modes(TTrack &Track); void render_turnout_ui(); void render_path_parameters(TTrack &Track); void render_straight_ui(); track_tab m_track_tab{track_tab::straights}; - int m_track_tab_request{-1}; void delete_selected_track(); void start_straight_gesture(int const Tool); void finish_straight_gesture(); void toggle_straight_set(TTrack &Track); + struct key_hint + { + char const *key; + std::string action; + }; + std::vector track_key_hints() const; + std::string track_readout() const; void draw_track_hints(); bool m_route_tab{true}; + + // vertical profile (grade line) along a route + struct profile_state + { + bool open{false}; + TTrack *from{nullptr}; + TTrack *to{nullptr}; + TTrack *picked_from{nullptr}; + TTrack *picked_to{nullptr}; + double run_length{3000.0}; + editor_track::route route; + std::vector samples; + std::vector terrain; // NaN where there's no terrain + profile::line line; + profile::context context; + std::vector issues; + double origin{0.0}; // chainage of the start of the route + double departure{0.0}; // largest difference between the grade line and the track + double view_from{0.0}; + double view_to{100.0}; + double view_centre{0.0}; + float exaggeration{20.0f}; + bool show_terrain{true}; + bool show_track{true}; + bool show_plan{true}; + int selected{-1}; + int dragging{-1}; + int curve_grip{-1}; + bool panning{false}; + bool changed{false}; + double hover{-1.0}; + std::string status; + std::string error; + }; + profile_state m_profile; + // grade lines designed in the editor, kept in the scenery files as "//$p" comment lines + struct stored_profile + { + scene::layer_handle layer{null_handle}; + double length{0.0}; + glm::dvec3 start{0.0}, end{0.0}, middle{0.0}; // of the route, world space + double origin{0.0}; + profile::line line; + }; + std::vector m_profiles; + bool m_profiles_loaded{false}; + void profile_library_load(); + void profile_library_mark(std::vector Layers); + int profile_find(bool &Reversed) const; + bool profile_restore(); + void profile_store(); + void profile_forget(); + void profile_open_stored(std::size_t const Index); + void profile_edited(); + void render_profile_window(); + void render_profile_source(); + bool render_profile_parameters(); + void render_profile_issues(); + void profile_open(TTrack *From, TTrack *To); + void profile_open_run(TTrack &Track); + void profile_take(editor_track::route Route, TTrack *From, TTrack *To); + void profile_resample(); + void profile_recognize(); + void profile_check(); + void profile_apply(); + void profile_fit_view(); + void profile_after_undo(); + struct profile_view; + void render_profile_canvas(); + void profile_fit_exaggeration(profile_view const &View); + void profile_navigate(profile_view const &View, bool const Hovered); + bool profile_canvas_edit(profile_view const &View, bool const Hovered); + void draw_profile_canvas(profile_view const &View, ImDrawList &Draw) const; + void profile_canvas_tooltip() const; + void render_profile_point(); + void draw_profile_overlay() const; + + // infrastructure along the track: objects bound to the paths follow their changes. bindings are kept in the + // scenery files as "//$b" comment lines + struct infra_candidate + { + infra::binding binding; + double offset{0.0}; // sideways from the axis + std::string reason; + bool chosen{false}; + bool bound{false}; + }; + struct infra_window + { + bool open{false}; + int scope{1}; // 0: the selected path, 1: the line through it, 2: the route of the vertical profile + float reach{1500.0f}; + float corridor{8.0f}; + bool follow{true}; + bool show{true}; + std::vector rules{infra::default_rules()}; + std::vector tracks; // of the scope + std::vector candidates; + int hovered{-1}; + std::string status; + std::string error; + }; + infra_window m_infra; + std::vector m_bindings; + std::vector> m_bindings_unresolved; // lines kept as they are + bool m_bindings_loaded{false}; + std::vector m_infra_buffer; // states of the objects before the change in progress + bool m_infra_suspended{false}; + void infra_load(); + void infra_store(); + std::vector infra_scope() const; + void infra_recognize(); + void infra_bind_chosen(); + void infra_unbind(std::vector Indices); + infra::binding *infra_find(infra::binding const &Binding); + infra::object_state infra_state_of(infra::binding const &Binding) const; + void infra_restore_state(infra::object_state const &State); + void infra_buffer(infra::binding const &Binding, bool const Refresh); + // editor_track::observer + void track_captured(TTrack const &Track) override; + void tracks_committed(std::vector const &Tracks) override; + void track_retired(TTrack &Track) override; + void track_split(TTrack &Original, TTrack &Created, segment_data const &First) override; + void infra_move(infra::binding &Binding); + void infra_attach(EditorSnapshot &Snapshot); + void render_infra_window(); + void infra_rebase(); + void render_infra_search(); + void render_infra_candidates(); + void render_infra_bound(); + void draw_infra_overlay() const; + bool m_route_gizmo_using{false}; glm::mat4 m_route_gizmo{1.0f}; float m_track_snap_radius{1.0f}; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp new file mode 100644 index 00000000..64ff2e61 --- /dev/null +++ b/application/editormodegauge.cpp @@ -0,0 +1,798 @@ +/* +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 "editor/editorFormat.hpp" +#include "editor/editorGauge.hpp" +#include "editor/editorGeometry.hpp" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/editoroverlay.h" +#include "rendering/renderer.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using geometry::plan_of; + +namespace +{ + +char const *const gauge_file{"editor_structure_gauges.txt"}; +// past this distance of the camera from the origin of the overlay it's rebuilt, to keep its float precision +double const gauge_origin_range{2000.0}; +// length of the line shown each way from the selected path, and around a hit +double const gauge_reach{500.0}; +double const gauge_spot_reach{60.0}; +// time spent on the scan of the whole map in a frame +auto const gauge_slice{std::chrono::milliseconds(8)}; +// distance of the camera from the place of a hit, along the track +double const gauge_view_distance{4.0}; + +// sampled line of the tracks with the widening of the gauge along it +struct gauge_line +{ + gauge::profile const *profile{nullptr}; + editor_track::route route; + std::vector samples; + std::vector sections; +}; + +// half of the carriageway of a road at the sample, it narrows linearly to the width at the end +double carriageway(editor_track::route_span const &Span, double const Chainage) +{ + auto const &track{*Span.track}; + auto const end{track.fTrackWidth2 > 0.f ? track.fTrackWidth2 : track.fTrackWidth}; + auto t{Span.to > Span.from ? std::clamp((Chainage - Span.from) / (Span.to - Span.from), 0.0, 1.0) : 0.0}; + if (false == Span.forward) + t = 1.0 - t; + return 0.5 * std::abs(track.fTrackWidth + (end - track.fTrackWidth) * t); +} + +gauge_line make_line(editor_track::route &Route, gauge::profile const &Profile) +{ + gauge_line line; + line.profile = &Profile; + line.samples = editor_track::sample_route(Route, 1.0); + line.route = Route; + bool const road{Profile.kind == gauge::kind::road}; + std::vector chainage, curvature, cant; + for (auto &sample : line.samples) + { + if (false == road) + sample.position.y += gauge::rail_height; + chainage.push_back(sample.chainage); + curvature.push_back(sample.curvature); + cant.push_back(sample.cant); + } + line.sections = gauge::sections(Profile.kind, chainage, curvature, cant); + if (road) + for (std::size_t i = 0; i < line.samples.size(); ++i) + line.sections[i].base = carriageway(line.route.spans[line.samples[i].span], line.samples[i].chainage); + return line; +} + +// outline checked along the path, none on the pavements, which are the roads with the pavement texture +gauge::profile const *outline_of(gauge::choice const &Choice, TTrack const &Track) +{ + auto const *profile{Choice.of(Track.iCategoryFlag)}; + if (profile == nullptr || profile->outline.size() < 2) + return nullptr; + if (profile == Choice.road) + { + auto name{editor_track::material_name(Track.m_material1)}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const C) { return static_cast(std::tolower(C)); }); + if (name.find("chodnik") != std::string::npos) + return nullptr; + } + return profile; +} + +// the line along the route from the path with the outline of its kind, none without the outline or the length +std::optional track_line(gauge::choice const &Choice, TTrack const &Track, editor_track::route &Route) +{ + auto const *profile{outline_of(Choice, Track)}; + if (profile == nullptr) + return std::nullopt; + auto line{make_line(Route, *profile)}; + if (line.samples.size() < 2) + return std::nullopt; + return line; +} + +// lines through all paths of the scenery, each path in one of them at least +std::vector scenery_lines(gauge::choice const &Choice) +{ + std::vector lines; + std::unordered_set visited; + auto const seen{[&](editor_track::route_span const &Span) { return visited.count(Span.track) > 0; }}; + auto const checked{[&](TTrack const *Track) { return Track != nullptr && editor_track::is_supported(*Track) && outline_of(Choice, *Track) != nullptr; }}; + for (auto *track : simulation::Paths.sequence()) + { + if (false == checked(track) || visited.count(track) > 0) + continue; + auto route{editor_track::run_route(*track, 1e7)}; + // the line may run on over the paths already covered, one of them is enough for the widening ahead of a curve + auto &spans{route.spans}; + while (spans.size() > 2 && seen(spans[0]) && seen(spans[1])) + spans.erase(spans.begin()); + while (spans.size() > 2 && seen(spans[spans.size() - 1]) && seen(spans[spans.size() - 2])) + spans.pop_back(); + for (auto const &span : spans) + visited.insert(span.track); + lines.push_back(make_line(route, *outline_of(Choice, *track))); + } + // diverging paths of the switches + for (auto *track : simulation::Paths.sequence()) + if (checked(track) && track->eType == tt_Switch) + { + editor_track::route route; + route.spans.push_back({track, 1, true}); + lines.push_back(make_line(route, *outline_of(Choice, *track))); + } + return lines; +} + +// samples of the lines, found by the position in the plan +class corridor +{ +public: + explicit corridor(std::vector const &Lines) : m_lines{Lines} + { + for (std::uint32_t line = 0; line < Lines.size(); ++line) + for (std::uint32_t i = 0; i < Lines[line].samples.size(); ++i) + { + // with the room for the widening in the sharpest curves on the largest cant + m_margin = std::max(m_margin, gauge::widest(*Lines[line].profile) + Lines[line].sections[i].base + 1.2); + auto const point{plan_of(Lines[line].samples[i].position)}; + m_cells[key(point, cell)].push_back({line, i}); + m_regions.insert(key(point, region)); + } + } + + static glm::dvec2 normal(editor_track::route_sample const &Sample) + { + return {-Sample.direction.y, Sample.direction.x}; + } + + // whether the sphere may reach the gauge of a line sampled in the cells around it + bool close(glm::dvec3 const &Center, double const Radius) const + { + auto const center{plan_of(Center)}; + auto const reach{Radius + m_margin}; + auto const from{index(center - reach, cell)}; + auto const to{index(center + reach, cell)}; + for (auto x = from.x; x <= to.x; ++x) + for (auto y = from.y; y <= to.y; ++y) + if (m_cells.count(key(x, y)) > 0) + return true; + return false; + } + + // whether the sphere may reach the gauge, roughly + bool nearby(glm::dvec3 const &Center, double const Radius) const + { + auto const center{plan_of(Center)}; + auto const reach{Radius + m_margin + region}; + auto const from{index(center - reach, region)}; + auto const to{index(center + reach, region)}; + for (auto x = from.x; x <= to.x; ++x) + for (auto y = from.y; y <= to.y; ++y) + if (m_regions.count(key(x, y)) > 0) + return true; + return false; + } + + struct hit + { + double depth{0.0}; // not positive outside of the gauge + editor_track::route_span const *span{nullptr}; // of the line the point enters + glm::dvec2 direction{0.0}; // of the line there, in the plan + }; + // deepest intrusion of the point into the gauge of the lines nearby + hit intrusion(glm::dvec3 const &Point) const + { + auto const point{plan_of(Point)}; + auto const center{index(point, cell)}; + // nearest sample of every line around + std::vector> nearest; + for (auto x = center.x - 1; x <= center.x + 1; ++x) + for (auto y = center.y - 1; y <= center.y + 1; ++y) + { + auto const found{m_cells.find(key(x, y))}; + if (found == m_cells.end()) + continue; + for (auto const &candidate : found->second) + { + auto const distance{glm::length2(plan_of(sample(candidate).position) - point)}; + if (distance > m_margin * m_margin) + continue; + auto const same{std::find_if(nearest.begin(), nearest.end(), [&](auto const &Other) { return Other.first.line == candidate.line; })}; + if (same == nearest.end()) + nearest.push_back({candidate, distance}); + else if (distance < same->second) + *same = {candidate, distance}; + } + } + hit deepest; + for (auto const &[found, distance] : nearest) + { + auto const &line{m_lines[found.line]}; + auto const &sample{line.samples[found.index]}; + auto const offset{point - plan_of(sample.position)}; + auto const along{glm::dot(offset, sample.direction)}; + if ((found.index == 0 && along < -0.6) || (found.index + 1 == line.samples.size() && along > 0.6)) + continue; + gauge::ring(*line.profile, line.sections[found.index], m_ring); + auto const depth{gauge::intrusion(m_ring, {glm::dot(offset, normal(sample)), Point.y - (sample.position.y + sample.grade * along)})}; + if (depth > deepest.depth) + deepest = {depth, &line.route.spans[sample.span], sample.direction}; + } + return deepest; + } + +private: + static constexpr double cell{8.0}; + static constexpr double region{64.0}; + + struct entry + { + std::uint32_t line; + std::uint32_t index; + }; + + static glm::i64vec2 index(glm::dvec2 const &Point, double const Size) + { + return {static_cast(std::floor(Point.x / Size)), static_cast(std::floor(Point.y / Size))}; + } + static std::uint64_t key(std::int64_t const X, std::int64_t const Y) + { + return (static_cast(X) << 32) ^ static_cast(Y); + } + static std::uint64_t key(glm::dvec2 const &Point, double const Size) + { + auto const cell{index(Point, Size)}; + return key(cell.x, cell.y); + } + editor_track::route_sample const &sample(entry const &Entry) const + { + return m_lines[Entry.line].samples[Entry.index]; + } + + std::vector const &m_lines; + double m_margin{0.0}; + mutable std::vector m_ring; // reused by the checks + std::unordered_map> m_cells; + std::unordered_set m_regions; +}; + +glm::dmat4 placement(TAnimModel const &Instance) +{ + auto const angles{Instance.Angles()}; + auto matrix{glm::translate(glm::dmat4(1.0), Instance.location())}; + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.y)), glm::dvec3(0.0, 1.0, 0.0)); + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.x)), glm::dvec3(1.0, 0.0, 0.0)); + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.z)), glm::dvec3(0.0, 0.0, 1.0)); + return glm::scale(matrix, glm::dvec3(Instance.Scale())); +} + +// calls the visitor with the world corners of the visible triangles of the submodel, its siblings and children +template void visit_triangles(TSubModel *Submodel, glm::dmat4 const &Matrix, Visitor const &Visit) +{ + for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->NextGet()) + { + if (submodel->iVisible == 0) + continue; + auto matrix{Matrix}; + if ((submodel->iFlags & 0xC000) && submodel->GetMatrix() != nullptr) + matrix *= glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray())); + if (submodel->eType == GL_TRIANGLES && submodel->m_geometry.handle != null_handle) + { + auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; + auto const corner{[&](std::size_t const Index) { return glm::dvec3(matrix * glm::dvec4(glm::dvec3(vertices[Index].position), 1.0)); }}; + auto const count{indices.empty() ? vertices.size() : indices.size()}; + for (std::size_t i = 0; i + 2 < count; i += 3) + { + if (indices.empty()) + Visit(corner(i), corner(i + 1), corner(i + 2)); + else + Visit(corner(indices[i]), corner(indices[i + 1]), corner(indices[i + 2])); + } + } + visit_triangles(submodel->ChildGet(), matrix, Visit); + } +} + +// the deepest place of the model in the gauge, added to the hits when it enters it +void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) +{ + if (false == Instance.visible() || Instance.Model() == nullptr || Instance.Model()->GetSMRoot() == nullptr) + return; + if (false == Space.nearby(Instance.location(), Instance.radius())) + return; + editor_mode::gauge_hit hit{Instance.name(), Instance.location()}; + // the triangle enters the gauge when any point of a grid spread over it does + visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { + auto const middle{(A + B + C) / 3.0}; + if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)}))) + return; + auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})}; + auto const steps{std::clamp(static_cast(std::ceil(edge / 0.5)), 1, 24)}; + for (int u = 0; u <= steps; ++u) + for (int v = 0; u + v <= steps; ++v) + { + auto const point{A + (B - A) * (static_cast(u) / steps) + (C - A) * (static_cast(v) / steps)}; + auto const found{Space.intrusion(point)}; + if (found.depth <= 0.0) + continue; + if (found.depth > hit.depth) + { + hit.depth = found.depth; + hit.point = point; + hit.track = found.span->track; + hit.path = found.span->path; + hit.direction = found.direction; + } + return; + } + }); + if (hit.depth > 0.0) + Hits.push_back(std::move(hit)); +} + +void sort_hits(std::vector &Hits) +{ + std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); +} + +// the models entering the gauge, the deepest first +void scan_models(corridor const &Space, std::vector &Hits) +{ + for (auto *instance : simulation::Instances.sequence()) + if (instance != nullptr) + scan_model(Space, *instance, Hits); + sort_hits(Hits); +} + +// translucent tunnel of the gauge along the line, with the rings every 10 m and the lines along its corners +void build_tunnel(gauge_line const &Line, std::vector &Surface, std::vector &Edges) +{ + std::vector places; + auto const ring{[&](std::size_t const Index) { + gauge::ring(*Line.profile, Line.sections[Index], places); + auto const &sample{Line.samples[Index]}; + auto const normal{corridor::normal(sample)}; + std::vector points; + for (auto const &place : places) + points.push_back(sample.position + glm::dvec3(normal.x * place.lateral, place.height, normal.y * place.lateral)); + return points; + }}; + std::vector previous; + double lastring{-1e9}; + for (std::size_t i = 0; i < Line.samples.size(); i += 2) + { + auto const current{ring(i)}; + if (false == previous.empty()) + for (std::size_t j = 1; j < current.size(); ++j) + { + Surface.insert(Surface.end(), {previous[j - 1], previous[j], current[j], previous[j - 1], current[j], current[j - 1]}); + Edges.insert(Edges.end(), {previous[j], current[j]}); + } + if (Line.samples[i].chainage - lastring >= 10.0 || i + 2 >= Line.samples.size()) + { + lastring = Line.samples[i].chainage; + for (std::size_t j = 1; j < current.size(); ++j) + Edges.insert(Edges.end(), {current[j - 1], current[j]}); + } + previous = current; + } +} + +// table of the points of the outline with the inserting and removing of the points, whether it changed +bool edit_points(std::vector &Points) +{ + bool edited{false}; + bool inserted{false}; + int remove{-1}; + ImGui::Columns(3, "gaugeoutline", false); + ImGui::SetColumnWidth(0, 110.0f); + ImGui::SetColumnWidth(1, 110.0f); + ImGui::TextDisabled("Half-width"); + ImGui::NextColumn(); + ImGui::TextDisabled("Height"); + ImGui::NextColumn(); + ImGui::NextColumn(); + for (int i = 0; i < static_cast(Points.size()); ++i) + { + ImGui::PushID(i); + auto &point{Points[i]}; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputDouble("##width", &point.half_width, 0.0, 0.0, "%.3f"); + edited |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputDouble("##height", &point.height, 0.0, 0.0, "%.3f"); + edited |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::NextColumn(); + if (ImGui::SmallButton("+")) + { + auto const &next{Points[std::min(i + 1, Points.size() - 1)]}; + Points.insert(Points.begin() + i + 1, {0.5 * (point.half_width + next.half_width), 0.5 * (point.height + next.height)}); + inserted = edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Insert a point after this one"); + ImGui::SameLine(); + if (Points.size() > 2 && ImGui::SmallButton("x")) + remove = i; + ImGui::NextColumn(); + ImGui::PopID(); + if (inserted) + break; + } + ImGui::Columns(1); + if (remove >= 0) + { + Points.erase(Points.begin() + remove); + edited = true; + } + return edited; +} + +} // namespace + +struct gauge_scan +{ + std::optional railway; + std::optional road; + std::vector lines; + std::optional space; // of the lines + std::size_t next{0}; // instance to check + float progress{0.f}; + std::chrono::steady_clock::time_point started; +}; + +void editor_mode::gauge_load() +{ + if (m_gauge.profiles.empty()) + m_gauge.profiles = gauge::load_profiles(gauge_file); + // the chosen railway and road outlines, the first ones of the kind when there's none + auto const fits{[&](int const Index, bool const Road) { + return Index >= 0 && Index < static_cast(m_gauge.profiles.size()) && (m_gauge.profiles[Index].kind == gauge::kind::road) == Road; + }}; + for (auto [index, road] : {std::pair{&m_gauge.profile, false}, std::pair{&m_gauge.road, true}}) + if (false == fits(*index, road)) + { + *index = -1; + for (int i = 0; i < static_cast(m_gauge.profiles.size()) && *index < 0; ++i) + if (fits(i, road)) + *index = i; + } +} + +gauge::choice editor_mode::gauge_choice() const +{ + auto const pick{[&](int const Index) { return Index >= 0 ? &m_gauge.profiles[Index] : nullptr; }}; + return {pick(m_gauge.profile), pick(m_gauge.road)}; +} + +std::vector const &editor_mode::gauge_hits() const +{ + return m_gauge.map.scanned ? m_gauge.map.hits : m_gauge.line.hits; +} + +void editor_mode::update_gauge() +{ + if (m_gauge.scan) + step_gauge_map(); + if (false == m_gauge.open) + { + if (false == EditorOverlay.batches.empty()) + EditorOverlay.clear(); + m_gauge.published = false; + return; + } + auto *track{m_gauge.enabled ? selected_track() : nullptr}; + // the geometry is rescanned once the edit ends + if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging) + m_gauge.pending = true; + else if (track == nullptr) + { + if (m_gauge.track != nullptr) + { + m_gauge.line = {}; + m_gauge.track = nullptr; + m_gauge.published = false; + } + } + else if (track != m_gauge.track || m_history.size() != m_gauge.history || m_gauge.pending) + { + m_gauge.track = track; + m_gauge.history = m_history.size(); + m_gauge.pending = false; + scan_gauge(*track); + m_gauge.published = false; + } + if (false == m_gauge.published || glm::distance(Camera.Pos, EditorOverlay.origin) > gauge_origin_range) + gauge_publish(); +} + +void editor_mode::scan_gauge(TTrack &Track) +{ + gauge_load(); + m_gauge.line = {}; + auto route{editor_track::run_route(Track, gauge_reach)}; + auto line{track_line(gauge_choice(), Track, route)}; + if (false == line.has_value()) + return; + std::vector const lines{std::move(*line)}; + corridor const space{lines}; + build_tunnel(lines.front(), m_gauge.line.surface, m_gauge.line.edges); + scan_models(space, m_gauge.line.hits); + if (false == m_gauge.map.scanned) + m_gauge.current = -1; +} + +void editor_mode::scan_gauge_map() +{ + gauge_load(); + auto scan{std::make_shared()}; + scan->started = std::chrono::steady_clock::now(); + auto const chosen{gauge_choice()}; + // copies, the outlines may be edited while it runs + scan->railway = chosen.railway != nullptr ? std::optional{*chosen.railway} : std::nullopt; + scan->road = chosen.road != nullptr ? std::optional{*chosen.road} : std::nullopt; + scan->lines = scenery_lines({scan->railway ? &*scan->railway : nullptr, scan->road ? &*scan->road : nullptr}); + scan->space.emplace(scan->lines); + m_gauge.map = {}; + m_gauge.map.scanned = true; + m_gauge.map.history = m_history.size(); + auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; + m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); + m_gauge.current = -1; + m_gauge.published = false; + m_gauge.scan = std::move(scan); +} + +void editor_mode::step_gauge_map() +{ + auto &scan{*m_gauge.scan}; + auto const &instances{simulation::Instances.sequence()}; + auto const until{std::chrono::steady_clock::now() + gauge_slice}; + while (scan.next < instances.size() && std::chrono::steady_clock::now() < until) + { + if (auto *instance{instances[scan.next++]}) + scan_model(*scan.space, *instance, m_gauge.map.hits); + } + scan.progress = instances.empty() ? 1.f : static_cast(scan.next) / instances.size(); + if (scan.next >= instances.size()) + finish_gauge_map(); +} + +void editor_mode::finish_gauge_map() +{ + auto const scan{std::move(m_gauge.scan)}; + auto &hits{m_gauge.map.hits}; + // the hit the camera went to stays chosen + auto const chosen{m_gauge.current >= 0 && m_gauge.current < static_cast(hits.size()) ? std::optional{hits[m_gauge.current].point} : std::nullopt}; + sort_hits(hits); + if (chosen) + m_gauge.current = static_cast(std::find_if(hits.begin(), hits.end(), [&](auto const &Hit) { return Hit.point == *chosen; }) - hits.begin()); + WriteLog("Structure gauge (" + m_gauge.map.profile + "): " + std::to_string(hits.size()) + " models enter it along " + std::to_string(scan->lines.size()) + " lines, scanned in " + + std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - scan->started).count()) + " ms"); + if (false == chosen.has_value() && false == hits.empty()) + gauge_focus(0); +} + +void editor_mode::gauge_publish() +{ + m_gauge.published = true; + EditorOverlay.clear(); + EditorOverlay.origin = Camera.Pos; + auto const batch{[](glm::vec4 const &Color, unsigned int const Type, std::vector const &Points) { + gfx::editor_overlay::batch result{Color, Type}; + result.points.reserve(Points.size()); + for (auto const &point : Points) + result.points.emplace_back(point - EditorOverlay.origin); + return result; + }}; + std::vector marker; + auto const &hits{gauge_hits()}; + if (m_gauge.current >= 0 && m_gauge.current < static_cast(hits.size())) + { + auto const point{hits[m_gauge.current].point}; + marker = {point - glm::dvec3(0.0, 3.0, 0.0), point + glm::dvec3(0.0, 3.0, 0.0), point - glm::dvec3(1.0, 0.0, 0.0), point + glm::dvec3(1.0, 0.0, 0.0), + point - glm::dvec3(0.0, 0.0, 1.0), point + glm::dvec3(0.0, 0.0, 1.0)}; + } + EditorOverlay.batches = {batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, m_gauge.line.surface), + batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.line.edges), + batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, marker), + batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, m_gauge.spot.surface), + batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.spot.edges)}; +} + +void editor_mode::gauge_focus(int const Index) +{ + auto const &hits{gauge_hits()}; + if (hits.empty()) + return; + m_gauge.current = (Index % static_cast(hits.size()) + static_cast(hits.size())) % static_cast(hits.size()); + m_gauge.published = false; + gauge_spot(hits[m_gauge.current]); + auto const &hit{hits[m_gauge.current]}; + // along the track, tangent to the gauge at the height of the place, from the end nearer to the camera + glm::dvec3 look{hit.direction.x, 0.0, hit.direction.y}; + look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); + if (glm::dot(Camera.Pos - hit.point, look) > 0.0) + look = -look; + m_focus_start_pos = Camera.Pos; + m_focus_start_angle = Camera.Angle; + m_focus_target_pos = hit.point - look * gauge_view_distance; + m_focus_target_angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(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::gauge_spot(gauge_hit const &Hit) +{ + m_gauge.spot = {}; + auto const &paths{simulation::Paths.sequence()}; + // the hit outlives the scan, the path may be gone since + if (Hit.track == nullptr || std::find(paths.begin(), paths.end(), Hit.track) == paths.end()) + return; + auto *track{const_cast(Hit.track)}; + editor_track::route route; + if (Hit.path == 0) + route = editor_track::run_route(*track, gauge_spot_reach); + else + route.spans.push_back({track, Hit.path, true}); + if (auto const line{track_line(gauge_choice(), *track, route)}) + build_tunnel(*line, m_gauge.spot.surface, m_gauge.spot.edges); +} + +void editor_mode::render_gauge_window() +{ + if (false == m_gauge.open) + return; + ImGui::SetNextWindowSize(ImVec2(380.0f, 420.0f), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin("Structure gauge", &m_gauge.open)) + { + ImGui::End(); + return; + } + if (ImGui::Checkbox("Show along the selected path", &m_gauge.enabled)) + m_gauge.pending = true; + 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."); + gauge_load(); + bool changed{render_gauge_choice("Railway", m_gauge.profile, false)}; + 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."); + changed |= render_gauge_choice("Road", m_gauge.road, true); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Clearance over the roads, from 0.5 m beyond the edges of the carriageway"); + + 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())}; + ImGui::ProgressBar(m_gauge.scan->progress, ImVec2(-70.0f, 0.0f), label.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Cancel")) + finish_gauge_map(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Stops the scan and keeps the models found so far"); + } + else if (ImGui::Button("Find the violations on the whole map")) + scan_gauge_map(); + 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"); + if (m_gauge.map.scanned && false == static_cast(m_gauge.scan)) + { + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) + { + m_gauge.map = {}; + m_gauge.current = -1; + m_gauge.published = false; + } + 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()); + } + render_gauge_hits(); + changed |= render_gauge_outline(m_gauge.profile); + changed |= render_gauge_outline(m_gauge.road); + if (changed) + m_gauge.pending = true; + ImGui::End(); +} + +void editor_mode::render_gauge_hits() +{ + auto const &hits{gauge_hits()}; + if (false == m_gauge.map.scanned && (false == m_gauge.enabled || m_gauge.track == nullptr)) + return; + if (hits.empty()) + { + ImGui::TextDisabled(m_gauge.map.scanned ? "No model enters the gauge on the map" : "The gauge is clear along this line"); + 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" : ""); + if (ImGui::ArrowButton("##gaugeprevious", ImGuiDir_Left)) + gauge_focus(m_gauge.current - 1); + ImGui::SameLine(); + if (ImGui::ArrowButton("##gaugenext", ImGuiDir_Right)) + gauge_focus(m_gauge.current + 1); + ImGui::SameLine(); + ImGui::Text("%d / %zu", m_gauge.current + 1, hits.size()); + ImGui::BeginChild("gaugehits", ImVec2(0.0f, std::min(160.0f, ImGui::GetTextLineHeightWithSpacing() * hits.size() + 8.0f)), true); + ImGuiListClipper clipper(static_cast(hits.size())); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) + { + auto const &hit{hits[i]}; + auto const label{(hit.model.empty() ? std::string{"(unnamed)"} : hit.model) + " " + std::to_string(static_cast(std::lround(hit.depth * 100.0))) + " cm##" + std::to_string(i)}; + if (ImGui::Selectable(label.c_str(), m_gauge.current == i)) + gauge_focus(i); + } + ImGui::EndChild(); +} + +bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road) +{ + bool changed{false}; + ImGui::SetNextItemWidth(-60.0f); + if (ImGui::BeginCombo(Label, Index >= 0 ? m_gauge.profiles[Index].name.c_str() : "none")) + { + for (int i = 0; i < static_cast(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)) + { + changed = i != Index; + Index = i; + } + ImGui::EndCombo(); + } + return changed; +} + +bool editor_mode::render_gauge_outline(int const Index) +{ + if (Index < 0) + return false; + auto &profile{m_gauge.profiles[Index]}; + if (false == ImGui::TreeNode(&profile, "Outline of %s (right half)", profile.name.c_str())) + return false; + ImGui::TextDisabled(profile.kind == gauge::kind::road ? "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."); + bool edited{edit_points(profile.outline)}; + if (ImGui::SmallButton("Restore the starting outlines")) + { + m_gauge.profiles = gauge::default_profiles(); + gauge_load(); + edited = true; + } + if (edited) + gauge::save_profiles(gauge_file, m_gauge.profiles); + ImGui::TreePop(); + return edited; +} diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp new file mode 100644 index 00000000..ba3213ff --- /dev/null +++ b/application/editormodeinfra.cpp @@ -0,0 +1,1387 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "editor/editorIncludeInfo.hpp" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Event.h" +#include "world/EvLaunch.h" +#include "world/MemCell.h" +#include "world/Track.h" +#include "world/Traction.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::plan_distance; +using geometry::plan_of; + +std::string const kBindingMark{"//$b"}; + +template +bool contains(std::vector const &Items, Type_ const &Item) +{ + return std::find(Items.begin(), Items.end(), Item) != Items.end(); +} + +ImU32 colour(infra::category const Category, int const Alpha = 255) +{ + switch (Category) + { + case infra::category::signal: return IM_COL32(255, 80, 80, Alpha); + case infra::category::sign: return IM_COL32(255, 220, 60, Alpha); + case infra::category::pole: return IM_COL32(160, 160, 255, Alpha); + case infra::category::catenary: return IM_COL32(80, 200, 255, Alpha); + case infra::category::logic: return IM_COL32(220, 120, 255, Alpha); + case infra::category::other: return IM_COL32(200, 200, 200, Alpha); + } + return IM_COL32(200, 200, 200, Alpha); +} + +// nodes still present in the scene; the editor deletes models outright +class live_nodes +{ + public: + void refresh() + { + std::array const sizes{simulation::Instances.sequence().size(), simulation::Traction.sequence().size(), simulation::Memory.sequence().size(), simulation::Events.launchers().size()}; + if (sizes == m_sizes && m_valid) + return; + m_sizes = sizes; + m_valid = true; + m_nodes.clear(); + for (auto *node : simulation::Instances.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Traction.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Memory.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Events.launchers()) + m_nodes.insert(node); + } + bool alive(scene::basic_node const *Node) const + { + return Node != nullptr && m_nodes.count(Node) > 0; + } + + private: + std::unordered_set m_nodes; + std::array m_sizes{}; + bool m_valid{false}; +}; +live_nodes Live; + +bool alive(infra::binding const &Binding) +{ + if (Binding.type == infra::kind::include) + { + if (false == scene::Layers.tracked(Binding.include)) + return false; + auto const &included{scene::Layers.instance(Binding.include)}; + return false == included.removed && false == included.dead; + } + return Live.alive(Binding.node); +} + +// placement parameters of an include +struct include_place +{ + std::string file; + std::vector values; + std::array position{-1, -1, -1}; // indices of the values + int yaw{-1}; + glm::dvec3 offset{0.0}; + std::string description; // name and category of the template +}; + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +bool include_place_of(scene::instance_handle const Instance, include_place &Place) +{ + if (false == scene::Layers.tracked(Instance)) + return false; + auto const &included{scene::Layers.instance(Instance)}; + if (included.removed || included.dead || included.file == nullptr) + return false; + // with a rotation or a scale in effect the parameters don't translate to a location in a single way + if (included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) + return false; + if (false == editor_includes::parse_directive(scene::Layers.directive(Instance), Place.file, Place.values)) + return false; + static std::map descriptions; + auto found{descriptions.find(*included.file)}; + if (found == descriptions.end()) + { + include_info info; + std::string error; + editor_includes::load(*included.file, info, error); + editor_includes::suggest(*included.file, info); + found = descriptions.emplace(*included.file, std::move(info)).first; + } + auto const &info{found->second}; + auto const index = [&](char const *Role) { + auto const result{editor_includes::parameter_with_role(info, Role) - 1}; + double value; + return (result >= 0 && result < static_cast(Place.values.size()) && numeric(Place.values[result], value)) ? result : -1; + }; + Place.position = {index("pos.x"), index("pos.y"), index("pos.z")}; + Place.yaw = index("rot.y"); + Place.offset = included.context.offset; + Place.description = info.name + ' ' + info.category; + return Place.position[0] >= 0 && Place.position[1] >= 0 && Place.position[2] >= 0; +} + +glm::dvec3 include_location(include_place const &Place) +{ + glm::dvec3 result{Place.offset}; + for (int axis = 0; axis < 3; ++axis) + { + double value{0.0}; + numeric(Place.values[Place.position[axis]], value); + result[axis] += value; + } + return result; +} + +// points which define the object, and its rotation around the vertical axis if it has one +bool object_points(infra::binding const &Binding, std::vector &Points, std::optional &Yaw) +{ + Points.clear(); + Yaw.reset(); + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + Points.push_back(model->location()); + Yaw = model->Angles().y; + return true; + } + case infra::kind::include: + { + include_place place; + if (false == include_place_of(Binding.include, place)) + return false; + Points.push_back(include_location(place)); + if (place.yaw >= 0) + { + double value{0.0}; + numeric(place.values[place.yaw], value); + Yaw = value; + } + return true; + } + case infra::kind::traction: + { + auto const *traction{static_cast(Binding.node)}; + Points = {traction->pPoint1, traction->pPoint2, traction->pPoint3, traction->pPoint4}; + return true; + } + case infra::kind::memcell: + case infra::kind::launcher: Points.push_back(Binding.node->location()); return true; + } + return false; +} + +std::string object_key(infra::binding const &Binding) +{ + std::string result; + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + result = model->Model() != nullptr ? model->Model()->NameGet() : std::string{}; + break; + } + case infra::kind::include: result = *scene::Layers.instance(Binding.include).file; break; + case infra::kind::traction: break; + case infra::kind::memcell: + case infra::kind::launcher: result = Binding.node->name(); break; + } + if (result.empty() || result.find_first_of(" \t\r\n") != std::string::npos) + result = "-"; + return result; +} + +std::string object_label(infra::binding const &Binding) +{ + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + auto name{model->Model() != nullptr ? model->Model()->NameGet() : std::string{}}; + if (false == model->name().empty() && model->name() != "none") + name = model->name() + " (" + name + ')'; + return name; + } + case infra::kind::include: return *scene::Layers.instance(Binding.include).file; + case infra::kind::traction: return Binding.node->name().empty() || Binding.node->name() == "none" ? std::string{"traction"} : Binding.node->name(); + case infra::kind::memcell: return "memcell " + Binding.node->name(); + case infra::kind::launcher: return "eventlauncher " + Binding.node->name(); + } + return {}; +} + +void move_model(TAnimModel *Model, glm::dvec3 const &Location, glm::vec3 const &Angles) +{ + if (glm::distance(Model->location(), Location) > 1e-6) + { + simulation::Region->erase(Model); + Model->location(Location); + simulation::Region->insert(Model); + } + Model->Angles(Angles); +} + +void move_traction(TTraction *Traction, std::array const &Points) +{ + if (Traction->pPoint1 == Points[0] && Traction->pPoint2 == Points[1] && Traction->pPoint3 == Points[2] && Traction->pPoint4 == Points[3]) + return; + // the height of the lowest dropper stays the same + auto const minheight{(Traction->pPoint3.y - Traction->pPoint1.y + Traction->pPoint4.y - Traction->pPoint2.y) * 0.5 - Traction->fHeightDifference}; + simulation::Region->erase_and_unregister(Traction); + Traction->pPoint1 = Points[0]; + Traction->pPoint2 = Points[1]; + Traction->pPoint3 = Points[2]; + Traction->pPoint4 = Points[3]; + Traction->fHeightDifference = (Points[2].y - Points[0].y + Points[3].y - Points[1].y) * 0.5 - minheight; + Traction->Init(); + Traction->location(glm::mix(Points[1], Points[0], 0.5)); + Traction->m_area.radius = -1.f; + simulation::Region->insert_and_register(Traction); + Traction->m_geometry = gfx::geometry_handle{}; + auto §ion{simulation::Region->section(Traction->location())}; + if (section.m_geometrycreated) + Traction->create_geometry(section.m_geometrybank); + Traction->mark_dirty(); +} + +void move_memcell(TMemCell *Memcell, glm::dvec3 const &Location) +{ + if (glm::distance(Memcell->location(), Location) <= 1e-6) + return; + simulation::Region->erase(Memcell); + Memcell->location(Location); + simulation::Region->insert(Memcell); +} + +void move_launcher(TEventLauncher *Launcher, glm::dvec3 const &Location) +{ + if (glm::distance(Launcher->location(), Location) <= 1e-6) + return; + // global and radio launchers aren't a part of the region + auto const placed{false == Launcher->IsGlobal() && false == Launcher->IsRadioActivated()}; + if (placed) + simulation::Region->erase(Launcher); + Launcher->location(Location); + if (placed) + simulation::Region->insert(Launcher); + Launcher->mark_dirty(); +} + +// bounding circle of the paths of a track, in the plan +struct track_area +{ + TTrack *track{nullptr}; + glm::dvec3 centre{0.0}; + double radius{0.0}; +}; + +track_area area_of(TTrack &Track) +{ + track_area result; + result.track = &Track; + std::vector points; + for (auto const &path : Track.m_paths) + { + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + points.insert(points.end(), {start, end, start + path.points[segment_data::point::control1], end + path.points[segment_data::point::control2]}); + } + if (points.empty()) + return result; + for (auto const &point : points) + result.centre += point / static_cast(points.size()); + for (auto const &point : points) + result.radius = std::max(result.radius, plan_distance(point, result.centre)); + return result; +} + +// the point lies sideways of the path, not beyond one of its ends +bool beside(infra::station const &Station, glm::dvec3 const &Point, bool const Interior) +{ + if (Interior) + return true; + auto const axis{infra::frame_at(Station)}; + return std::abs(glm::dot(plan_of(Point - axis.point), axis.forward)) <= 0.5; +} + +// nearest point of the tracks, sideways from the axis within the corridor +template +bool nearest_beside(Areas_ const &Areas, glm::dvec3 const &Point, double const Corridor, infra::station &Station) +{ + auto best{-1.0}; + for (auto const &entry : Areas) + { + track_area const &area = entry; + if (plan_distance(Point, area.centre) > area.radius + Corridor) + continue; + infra::station station; + bool interior; + auto const distance{infra::project(*area.track, Point, station, interior)}; + if (distance <= Corridor && (best < 0.0 || distance < best) && beside(station, Point, interior)) + { + best = distance; + Station = station; + } + } + return best >= 0.0; +} + +// paths of the whole scenery by cells of the plan, for the lookups reaching up to kReach from the axis +class track_index +{ + public: + static constexpr double kReach{55.0}; + void add(TTrack &Track) + { + auto const index{m_areas.size()}; + m_areas.push_back(area_of(Track)); + std::set cells; + for (auto const &path : Track.m_paths) + for (auto const &point : infra::outline(path, kCell * 0.5)) + for (auto x = key(point.x - kReach - kCell * 0.25); x <= key(point.x + kReach + kCell * 0.25); ++x) + for (auto z = key(point.z - kReach - kCell * 0.25); z <= key(point.z + kReach + kCell * 0.25); ++z) + cells.insert({x, z}); + for (auto const &cell : cells) + m_cells[cell].push_back(index); + } + bool nearest(glm::dvec3 const &Point, double const Corridor, infra::station &Station) const + { + auto const cell{m_cells.find({key(Point.x), key(Point.z)})}; + if (cell == m_cells.end()) + return false; + std::vector> areas; + for (auto const index : cell->second) + areas.emplace_back(m_areas[index]); + return nearest_beside(areas, Point, std::min(Corridor, kReach), Station); + } + bool empty() const + { + return m_areas.empty(); + } + + private: + static constexpr double kCell{64.0}; + using cell_key = std::pair; + static long long key(double const Value) + { + return static_cast(std::floor(Value / kCell)); + } + std::vector m_areas; + std::map> m_cells; +}; + +// lookup of points by a grid of 1 m cells in the plan +template +class point_grid +{ + public: + void add(glm::dvec3 const &Point, Type_ Item) + { + m_cells[key(Point)].emplace_back(Point, Item); + } + template + std::optional find(glm::dvec3 const &Point, double const Tolerance, Match_ const &Match) const + { + auto const centre{key(Point)}; + for (auto x = centre.first - 1; x <= centre.first + 1; ++x) + for (auto z = centre.second - 1; z <= centre.second + 1; ++z) + { + auto const cell{m_cells.find({x, z})}; + if (cell == m_cells.end()) + continue; + for (auto const &entry : cell->second) + if (glm::distance(entry.first, Point) <= Tolerance && Match(entry.second)) + return entry.second; + } + return std::nullopt; + } + + private: + using cell_key = std::pair; + static cell_key key(glm::dvec3 const &Point) + { + return {static_cast(std::floor(Point.x)), static_cast(std::floor(Point.z))}; + } + std::map>> m_cells; +}; + +double turn_of(infra::binding const &Binding, double const Yaw) +{ + auto const axis{infra::frame_at(Binding.anchors.front().at)}; + return clamp_circular(Yaw - infra::heading(axis.forward)); +} + +// the objects which the stored bindings can name, by their locations +struct bindable_objects +{ + point_grid paths; + point_grid models; + point_grid traction; + point_grid logic; + point_grid includes; + + bindable_objects() + { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (auto const &path : track->m_paths) + paths.add(path.points[segment_data::point::start], track); + for (auto *model : simulation::Instances.sequence()) + if (model != nullptr && false == model->from_template() && model->Model() != nullptr) + models.add(model->location(), model); + for (auto *piece : simulation::Traction.sequence()) + if (piece != nullptr && false == piece->from_template()) + traction.add(piece->pPoint1, piece); + for (auto *memcell : simulation::Memory.sequence()) + if (memcell != nullptr && false == memcell->from_template()) + logic.add(memcell->location(), memcell); + for (auto *launcher : simulation::Events.launchers()) + if (launcher != nullptr && false == launcher->from_template()) + logic.add(launcher->location(), launcher); + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (include_place_of(handle, place)) + includes.add(include_location(place), handle); + } + } +}; + +// format of the lines, coordinates in the space of the file: +// { } +// the object is the model, the include (position from its parameters), the traction piece (its first point), the memory +// cell or the event launcher found at the location; the anchors name the paths by their ends +std::optional read_binding(std::string const &Text, scene::layer_context const &Context, bindable_objects const &Objects) +{ + double const tolerance{0.05}; + std::istringstream line{Text}; + std::string kindname, key; + int group{0}, turns{0}, count{0}; + glm::dvec3 location; + line >> kindname >> group >> location.x >> location.y >> location.z >> key >> turns >> count; + if (line.fail() || count < 1 || count > 4) + return std::nullopt; + location = Context.to_world(location); + infra::binding binding; + binding.group = static_cast(std::clamp(group, 0, static_cast(infra::category::other))); + binding.turns = (turns != 0); + auto found{false}; + if (kindname == infra::name(infra::kind::model)) + { + binding.type = infra::kind::model; + auto const model{Objects.models.find(location, tolerance, [&](TAnimModel *Model) { return Model->Model()->NameGet() == key; })}; + if ((found = model.has_value())) + binding.node = *model; + } + else if (kindname == infra::name(infra::kind::include)) + { + binding.type = infra::kind::include; + auto const handle{Objects.includes.find(location, tolerance, [&](scene::instance_handle Handle) { return *scene::Layers.instance(Handle).file == key; })}; + if ((found = handle.has_value())) + binding.include = *handle; + } + else if (kindname == infra::name(infra::kind::traction)) + { + binding.type = infra::kind::traction; + auto const piece{Objects.traction.find(location, tolerance, [](TTraction *) { return true; })}; + if ((found = piece.has_value())) + binding.node = *piece; + } + else if (kindname == infra::name(infra::kind::memcell) || kindname == infra::name(infra::kind::launcher)) + { + binding.type = (kindname == infra::name(infra::kind::memcell) ? infra::kind::memcell : infra::kind::launcher); + auto const node{Objects.logic.find(location, tolerance, [&](scene::basic_node *Node) { + auto const ismemcell{dynamic_cast(Node) != nullptr}; + return ismemcell == (binding.type == infra::kind::memcell) && (key == "-" || Node->name() == key); + })}; + if ((found = node.has_value())) + binding.node = *node; + } + std::vector points; + std::optional yaw; + if (false == found || false == object_points(binding, points, yaw)) + return std::nullopt; + for (int i = 0; i < count && found; ++i) + { + int path{0}; + glm::dvec3 start, end; + line >> path >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z; + start = Context.to_world(start); + end = Context.to_world(end); + auto const track{Objects.paths.find(start, tolerance, [&](TTrack *Track) { + return path >= 0 && path < static_cast(Track->m_paths.size()) && + glm::distance(Track->m_paths[path].points[segment_data::point::start], start) <= tolerance && + glm::distance(Track->m_paths[path].points[segment_data::point::end], end) <= tolerance; + })}; + if (line.fail() || false == track.has_value()) + { + found = false; + break; + } + auto const &point{points[std::min(i, points.size() - 1)]}; + binding.anchors.push_back(infra::make_anchor(*track, path, point)); + } + if (false == found || binding.anchors.size() != points.size()) + return std::nullopt; + binding.turns = binding.turns && yaw.has_value(); + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + return binding; +} + +} // namespace + +void editor_mode::infra_load() +{ + if (m_bindings_loaded || scene::Layers.empty()) + return; + m_bindings_loaded = true; + auto const marked{scene::Layers.marked_layers(kBindingMark)}; + if (marked.empty()) + return; + + bindable_objects const objects; + auto resolved{0}; + for (auto const layer : marked) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + for (auto const &text : scene::Layers.marked(layer, kBindingMark)) + { + if (text.empty() || text.front() == '#') + continue; + auto binding{read_binding(text, context, objects)}; + if (false == binding.has_value()) + { + m_bindings_unresolved.emplace_back(layer, text); + continue; + } + m_bindings.push_back(std::move(*binding)); + ++resolved; + } + } + WriteLog("Editor: " + std::to_string(resolved) + " object(s) bound to the paths read from the scenery" + + (m_bindings_unresolved.empty() ? std::string{} : ", " + std::to_string(m_bindings_unresolved.size()) + " binding(s) whose objects or paths weren't found are kept as they are"), + logtype::generic); +} + +void editor_mode::infra_store() +{ + if (false == m_bindings_loaded || scene::Layers.empty()) + return; + Live.refresh(); + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), [](infra::binding const &Binding) { return false == alive(Binding); }), m_bindings.end()); + + std::map> lines; + for (auto const layer : scene::Layers.marked_layers(kBindingMark)) + lines[layer]; + for (auto const &entry : m_bindings_unresolved) + lines[entry.first].push_back(entry.second); + for (auto const &binding : m_bindings) + { + std::vector points; + std::optional yaw; + if (binding.anchors.empty() || false == object_points(binding, points, yaw)) + continue; + auto layer{scene::Layers.resolve(binding.anchors.front().at.track->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + auto text{std::string{infra::name(binding.type)} + ' ' + std::to_string(static_cast(binding.group)) + ' ' + local(points.front()) + ' ' + object_key(binding) + ' ' + + (binding.turns ? "1 " : "0 ") + std::to_string(binding.anchors.size())}; + for (auto const &anchor : binding.anchors) + { + auto const &path{anchor.at.track->m_paths[anchor.at.path]}; + text += ' ' + std::to_string(anchor.at.path) + ' ' + local(path.points[segment_data::point::start]) + ' ' + local(path.points[segment_data::point::end]); + } + lines[layer].push_back(std::move(text)); + } + for (auto &entry : lines) + { + if (false == entry.second.empty()) + entry.second.insert(entry.second.begin(), "# objects bound to the paths in the track editor, they follow changes of the track; the simulation skips these lines"); + scene::Layers.mark(entry.first, kBindingMark, std::move(entry.second)); + } +} + +std::vector editor_mode::infra_scope() const +{ + std::vector result; + auto const add = [&](TTrack *Track) { + if (Track != nullptr && false == Track->m_editorremoved && editor_track::is_supported(*Track) && false == contains(result, Track)) + result.push_back(Track); + }; + auto *selected{selected_track()}; + switch (m_infra.scope) + { + case 0: add(selected); break; + case 1: + if (selected != nullptr) + for (auto const &span : editor_track::run_route(*selected, m_infra.reach).spans) + add(span.track); + break; + case 2: + for (auto const &span : m_profile.route.spans) + add(span.track); + break; + case 3: + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && editor_track::is_supported(*track)) + result.push_back(track); + break; + default: break; + } + return result; +} + +infra::binding *editor_mode::infra_find(infra::binding const &Binding) +{ + for (auto &binding : m_bindings) + if (binding.same(Binding)) + return &binding; + return nullptr; +} + +void editor_mode::infra_recognize() +{ + auto &state{m_infra}; + infra_load(); + state.candidates.clear(); + state.hovered = -1; + state.error.clear(); + state.status.clear(); + state.tracks = infra_scope(); + 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"); + return; + } + track_index index; + std::unordered_set names; + for (auto *track : state.tracks) + { + index.add(*track); + if (false == track->name().empty() && track->name() != "none") + names.insert(track->name()); + } + Live.refresh(); + + auto const corridor{static_cast(state.corridor)}; + auto const add = [&](infra::binding Binding, std::string const &Reason, double const Reach) { + std::vector points; + std::optional yaw; + if (false == object_points(Binding, points, yaw)) + return; + for (auto const &point : points) + { + infra::station station; + if (false == index.nearest(point, Reach, station)) + return; + Binding.anchors.push_back(infra::make_anchor(station, point)); + } + if (std::abs(Binding.anchors.front().height) > 40.0) + return; + Binding.turns = yaw.has_value() && Binding.type != infra::kind::traction; + if (Binding.turns) + Binding.yaw = turn_of(Binding, *yaw); + Binding.label = object_label(Binding); + infra_candidate candidate; + candidate.offset = Binding.anchors.front().offset; + candidate.reason = Reason; + candidate.bound = (infra_find(Binding) != nullptr); + candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other); + candidate.binding = std::move(Binding); + state.candidates.push_back(std::move(candidate)); + }; + auto const rule_reason = [](infra::category const Group, char const *What) { return Group == infra::category::other ? std::string{"nearby, no rule matches the "} + What : std::string{"the "} + What + " matches a rule"; }; + + for (auto *model : simulation::Instances.sequence()) + { + // models of the includes follow along with their include + if (model == nullptr || model->from_template() || model->Model() == nullptr) + continue; + infra::binding binding; + binding.type = infra::kind::model; + binding.node = model; + binding.group = infra::classify(model->Model()->NameGet() + ' ' + model->name(), state.rules); + add(binding, rule_reason(binding.group, "file name"), corridor); + } + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (false == include_place_of(handle, place)) + continue; + infra::binding binding; + binding.type = infra::kind::include; + binding.include = handle; + binding.group = infra::classify(place.file + ' ' + place.description, state.rules); + add(binding, rule_reason(binding.group, "template or its description"), corridor); + } + for (auto *piece : simulation::Traction.sequence()) + { + if (piece == nullptr || piece->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::traction; + binding.node = piece; + binding.group = infra::category::catenary; + add(binding, "wire over the track", corridor); + } + for (auto *memcell : simulation::Memory.sequence()) + { + if (memcell == nullptr || memcell->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::memcell; + binding.node = memcell; + binding.group = infra::category::logic; + auto const named{false == memcell->asTrackName.empty() && names.count(memcell->asTrackName) > 0}; + add(binding, named ? "names the path " + memcell->asTrackName : std::string{"nearby"}, named ? std::max(corridor, 50.0) : corridor); + } + for (auto *launcher : simulation::Events.launchers()) + { + if (launcher == nullptr || launcher->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::launcher; + binding.node = launcher; + binding.group = infra::category::logic; + 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; }); + state.status = format("%d object(s) found along %d path(s)", static_cast(state.candidates.size()), static_cast(state.tracks.size())); +} + +void editor_mode::infra_bind_chosen() +{ + auto &state{m_infra}; + auto count{0}; + Live.refresh(); + for (auto &candidate : state.candidates) + { + if (false == candidate.chosen || candidate.bound || false == alive(candidate.binding)) + continue; + if (auto *existing{infra_find(candidate.binding)}) + *existing = candidate.binding; + else + m_bindings.push_back(candidate.binding); + candidate.bound = true; + candidate.chosen = false; + ++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); +} + +void editor_mode::infra_unbind(std::vector Indices) +{ + std::sort(Indices.begin(), Indices.end()); + Indices.erase(std::unique(Indices.begin(), Indices.end()), Indices.end()); + for (auto index{Indices.rbegin()}; index != Indices.rend(); ++index) + { + if (*index >= m_bindings.size()) + continue; + for (auto &candidate : m_infra.candidates) + if (candidate.binding.same(m_bindings[*index])) + candidate.bound = false; + m_bindings.erase(m_bindings.begin() + *index); + } +} + +infra::object_state editor_mode::infra_state_of(infra::binding const &Binding) const +{ + infra::object_state result; + result.saved = Binding; + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + result.location = model->location(); + result.angles = model->Angles(); + break; + } + case infra::kind::include: result.directive = scene::Layers.directive(Binding.include); break; + case infra::kind::traction: + { + auto const *traction{static_cast(Binding.node)}; + result.points = {traction->pPoint1, traction->pPoint2, traction->pPoint3, traction->pPoint4}; + break; + } + case infra::kind::memcell: + case infra::kind::launcher: result.location = Binding.node->location(); break; + } + return result; +} + +void editor_mode::infra_restore_state(infra::object_state const &State) +{ + Live.refresh(); + if (false == alive(State.saved)) + return; + if (auto *binding{infra_find(State.saved)}) + *binding = State.saved; + switch (State.saved.type) + { + case infra::kind::model: move_model(static_cast(State.saved.node), State.location, State.angles); break; + case infra::kind::include: + if (false == State.directive.empty() && State.directive != scene::Layers.directive(State.saved.include)) + set_include_directive(State.saved.include, State.directive); + break; + case infra::kind::traction: move_traction(static_cast(State.saved.node), State.points); break; + case infra::kind::memcell: move_memcell(static_cast(State.saved.node), State.location); break; + case infra::kind::launcher: move_launcher(static_cast(State.saved.node), State.location); break; + } +} + +void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh) +{ + for (auto &entry : m_infra_buffer) + { + if (false == entry.saved.same(Binding)) + continue; + if (Refresh && false == entry.moved) + entry = infra_state_of(Binding); + return; + } + if (false == Refresh && false == m_history.empty() && m_history.back().action == EditorSnapshot::Action::TrackEdit) + { + // a drag keeps a single undo step, which holds the state from before the drag already + for (auto const &entry : m_history.back().infra) + if (entry.saved.same(Binding)) + return; + } + m_infra_buffer.push_back(infra_state_of(Binding)); +} + +void editor_mode::track_captured(TTrack const &Track) +{ + if (m_infra_suspended || false == m_infra.follow) + return; + infra_load(); + if (m_bindings.empty()) + return; + Live.refresh(); + for (auto &binding : m_bindings) + { + if (std::none_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { return Anchor.at.track == &Track; }) || false == alive(binding)) + continue; + // an object moved by hand since it was bound is measured again, as long as the path is the way it was measured + std::vector points; + std::optional yaw; + if (object_points(binding, points, yaw) && points.size() == binding.anchors.size()) + { + for (std::size_t i = 0; i < points.size(); ++i) + { + auto &anchor{binding.anchors[i]}; + if (anchor.at.track != &Track || anchor.at.path >= static_cast(Track.m_paths.size())) + continue; + auto const &path{Track.m_paths[anchor.at.path]}; + if (glm::distance(path.points[segment_data::point::start], anchor.start) > 1e-4 || glm::distance(path.points[segment_data::point::end], anchor.end) > 1e-4) + continue; + auto placed{anchor}; + if (glm::distance(infra::place(placed), points[i]) <= 0.01) + continue; + anchor = infra::make_anchor(anchor.at.track, anchor.at.path, points[i]); + } + if (binding.turns && yaw.has_value() && binding.anchors.front().at.track == &Track) + binding.yaw = turn_of(binding, *yaw); + } + infra_buffer(binding, true); + } +} + +void editor_mode::tracks_committed(std::vector const &Tracks) +{ + if (m_infra_suspended || false == m_infra.follow) + return; + infra_load(); + if (m_bindings.empty()) + return; + Live.refresh(); + std::vector areas; + for (auto *track : Tracks) + if (false == track->m_editorremoved) + areas.push_back(area_of(*track)); + // paths which took over may have been committed before the old ones were retired + std::optional> everywhere; + auto const take_over = [&](glm::dvec3 const &Foot, infra::station &Station) { + if (nearest_beside(areas, Foot, 30.0, Station)) + return true; + if (false == everywhere.has_value()) + { + everywhere.emplace(); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && editor_track::is_supported(*track)) + everywhere->push_back(area_of(*track)); + } + // only a path running where the old one did, not the one alongside + return nearest_beside(*everywhere, Foot, 1.0, Station); + }; + for (auto &binding : m_bindings) + { + auto const affected{std::any_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { + return Anchor.at.track != nullptr && (Anchor.at.track->m_editorremoved || contains(Tracks, Anchor.at.track)); + })}; + if (false == affected || false == alive(binding)) + continue; + auto moved{binding}; + auto lost{false}; + for (auto &anchor : moved.anchors) + { + auto *track{anchor.at.track}; + if (false == track->m_editorremoved && false == contains(Tracks, track)) + continue; + // the object goes square to the new axis from the point of the old one, so a shift of the track carries it + // sideways rather than along; its own path goes first, then the ones which took over + infra::station station; + auto found{false}; + if (false == track->m_editorremoved && anchor.at.path < static_cast(track->m_paths.size())) + { + double s; + bool interior; + auto const distance{infra::project(track->m_paths[anchor.at.path], anchor.foot, s, interior)}; + station = {track, anchor.at.path, s}; + found = (distance <= 30.0 && beside(station, anchor.foot, interior)); + } + if (false == found && false == take_over(anchor.foot, station)) + { + lost = true; + break; + } + auto const offset{anchor.offset}; + auto const height{anchor.height}; + anchor = infra::make_anchor(station, anchor.foot); + anchor.offset = offset; + anchor.height = height; + } + if (lost) + { + binding.lost = true; + continue; + } + infra_buffer(binding, false); + moved.lost = false; + binding = std::move(moved); + infra_move(binding); + for (auto &entry : m_infra_buffer) + if (entry.saved.same(binding)) + entry.moved = true; + } +} + +void editor_mode::track_retired(TTrack &) +{ + tracks_committed({}); +} + +void editor_mode::track_split(TTrack &Original, TTrack &Created, segment_data const &First) +{ + if (m_infra_suspended || Created.m_paths.empty()) + return; + auto const length{infra::plan_length(First)}; + auto const &second{Created.m_paths.front()}; + for (auto &binding : m_bindings) + for (auto &anchor : binding.anchors) + { + if (anchor.at.track != &Original || anchor.at.path != 0) + continue; + if (anchor.at.s > length) + { + anchor.at.track = &Created; + anchor.at.s -= length; + anchor.start = second.points[segment_data::point::start]; + anchor.end = second.points[segment_data::point::end]; + anchor.length = infra::plan_length(second); + } + else + { + anchor.end = First.points[segment_data::point::end]; + anchor.length = length; + } + } +} + +void editor_mode::infra_move(infra::binding &Binding) +{ + if (Binding.anchors.empty()) + return; + auto const location{infra::place(Binding.anchors.front())}; + auto const facing = [&]() { return clamp_circular(infra::heading(infra::frame_at(Binding.anchors.front().at).forward) + Binding.yaw); }; + switch (Binding.type) + { + case infra::kind::model: + { + auto *model{static_cast(Binding.node)}; + auto angles{model->Angles()}; + if (Binding.turns) + angles.y = static_cast(facing()); + move_model(model, location, angles); + break; + } + case infra::kind::include: + { + include_place place; + if (false == include_place_of(Binding.include, place)) + return; + for (int axis = 0; axis < 3; ++axis) + place.values[place.position[axis]] = editor_includes::number(location[axis] - place.offset[axis]); + if (Binding.turns && place.yaw >= 0) + place.values[place.yaw] = editor_includes::number(facing()); + auto const directive{editor_includes::compose_directive(place.file, place.values)}; + if (directive != scene::Layers.directive(Binding.include)) + set_include_directive(Binding.include, directive); + break; + } + case infra::kind::traction: + { + if (Binding.anchors.size() != 4) + return; + std::array points; + for (std::size_t i = 0; i < 4; ++i) + points[i] = infra::place(Binding.anchors[i]); + move_traction(static_cast(Binding.node), points); + break; + } + case infra::kind::memcell: move_memcell(static_cast(Binding.node), location); break; + case infra::kind::launcher: move_launcher(static_cast(Binding.node), location); break; + } +} + +void editor_mode::infra_attach(EditorSnapshot &Snapshot) +{ + auto const involved = [&](infra::object_state const &State) { + if (State.moved) + return true; + for (auto const &anchor : State.saved.anchors) + { + if (contains(Snapshot.created, anchor.at.track)) + return true; + for (auto const &entry : Snapshot.tracks) + if (entry.first == anchor.at.track) + return true; + } + return false; + }; + for (auto &entry : m_infra_buffer) + if (involved(entry)) + Snapshot.infra.push_back(std::move(entry)); + m_infra_buffer.clear(); +} + +void editor_mode::render_infra_window() +{ + 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(); + Live.refresh(); + + auto follow{state.follow}; + if (ImGui::Checkbox("Bound objects follow the changes of the track", &follow)) + { + state.follow = follow; + if (follow) + infra_rebase(); + } + 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" + "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::Checkbox("Show in 3D", &state.show); + ImGui::Text("Objects bound in the scenery: %d", static_cast(m_bindings.size())); + if (false == m_bindings_unresolved.empty()) + { + 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(m_bindings_unresolved.size())); + } + + ImGui::Separator(); + render_infra_search(); + render_infra_candidates(); + render_infra_bound(); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.f, 0.4f, 0.35f, 1.f), "%s", state.error.c_str()); + else if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + ImGui::End(); +} + +// what changed while the bindings did not follow the track becomes their new starting point +void editor_mode::infra_rebase() +{ + for (auto &binding : m_bindings) + { + std::vector points; + std::optional yaw; + if (false == alive(binding) || false == object_points(binding, points, yaw) || points.size() != binding.anchors.size()) + continue; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto &anchor{binding.anchors[i]}; + if (anchor.at.track == nullptr || anchor.at.track->m_editorremoved || anchor.at.path >= static_cast(anchor.at.track->m_paths.size())) + { + binding.lost = true; + continue; + } + anchor = infra::make_anchor(anchor.at.track, anchor.at.path, points[i]); + } + if (binding.turns && yaw.has_value()) + binding.yaw = turn_of(binding, *yaw); + } +} + +void editor_mode::render_infra_search() +{ + auto &state{m_infra}; + static char const *const scopes[] = {"Selected path", "Line through the selected path", "Route of the vertical profile", "Whole scenery"}; + ImGui::SetNextItemWidth(260.f); + ImGui::Combo("Look along", &state.scope, scopes, IM_ARRAYSIZE(scopes)); + if (state.scope == 1) + { + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat("Reach each way, m", &state.reach, 100.f, 500.f, "%.0f"); + state.reach = std::clamp(state.reach, 10.f, 20000.f); + } + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat("Corridor each side of the axis, m", &state.corridor, 0.5f, 2.f, "%.1f"); + state.corridor = std::clamp(state.corridor, 0.5f, 50.f); + if (ImGui::TreeNode("Rules for names of the files")) + { + ImGui::TextDisabled("Parts of the names, separated by spaces; the first rule which matches decides"); + for (std::size_t i = 0; i < state.rules.size(); ++i) + { + auto &rule{state.rules[i]}; + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), "%s", rule.keywords.c_str()); + ImGui::SetNextItemWidth(360.f); + if (ImGui::InputText((std::string{infra::name(rule.group)} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer))) + rule.keywords = buffer; + } + if (ImGui::SmallButton("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" + "traction is always the catenary, memory cells and event launchers the logic"); + ImGui::TreePop(); + } + if (ImGui::Button("Find objects")) + infra_recognize(); + +} + +void editor_mode::render_infra_candidates() +{ + auto &state{m_infra}; + auto &candidates{state.candidates}; + auto const choose = [&](std::optional const Group, bool const Chosen) { + for (auto &candidate : candidates) + if (false == Group.has_value() || candidate.binding.group == *Group) + candidate.chosen = Chosen && false == candidate.bound; + }; + state.hovered = -1; + if (false == candidates.empty()) + { + int chosen{0}; + for (auto const &candidate : candidates) + chosen += candidate.chosen ? 1 : 0; + ImGui::SameLine(); + if (ImGui::Button(format("Bind the chosen (%d)", chosen).c_str())) + infra_bind_chosen(); + ImGui::SameLine(); + if (ImGui::SmallButton("All")) + choose({}, true); + ImGui::SameLine(); + if (ImGui::SmallButton("None")) + choose({}, false); + + ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); + for (auto const group : infra::categories()) + { + auto const count{std::count_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })}; + if (count == 0) + continue; + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group))); + auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast(count)).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{"all##g"} + infra::name(group)).c_str())) + choose(group, true); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{"none##g"} + infra::name(group)).c_str())) + choose(group, false); + if (false == open) + continue; + // the candidates are sorted by the category + auto const first{static_cast(std::distance(candidates.begin(), std::find_if(candidates.begin(), candidates.end(), [&](infra_candidate const &Candidate) { return Candidate.binding.group == group; })))}; + ImGuiListClipper clipper(static_cast(count)); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const i{static_cast(first + row)}; + auto &candidate{candidates[i]}; + ImGui::PushID(static_cast(i)); + if (candidate.bound) + { + ImGui::TextDisabled("[bound]"); + } + else + { + ImGui::Checkbox("##chosen", &candidate.chosen); + } + ImGui::SameLine(); + ImGui::Text("%s", candidate.binding.label.c_str()); + if (ImGui::IsItemHovered()) + { + state.hovered = static_cast(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(), + std::abs(candidate.offset), candidate.offset >= 0.0 ? "right" : "left", candidate.binding.anchors.front().height, candidate.reason.c_str()); + } + ImGui::SameLine(); + ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset); + ImGui::PopID(); + } + ImGui::TreePop(); + } + ImGui::EndChild(); + } +} + +void editor_mode::render_infra_bound() +{ + if (ImGui::TreeNode("Bound objects along the scope")) + { + auto const scope{infra_scope()}; + std::unordered_set const tracks(scope.begin(), scope.end()); + std::vector along; + 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; })) + along.push_back(i); + ImGui::Text("%d of %d", static_cast(along.size()), static_cast(m_bindings.size())); + if (false == along.empty()) + { + ImGui::SameLine(); + if (ImGui::SmallButton("Unbind all of these")) + { + infra_unbind(along); + along.clear(); + } + } + std::optional unbind; + ImGui::BeginChild("##infrabound", ImVec2(0.f, 160.f), true); + ImGuiListClipper clipper(static_cast(along.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const index{along[row]}; + auto const &binding{m_bindings[index]}; + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton("unbind")) + unbind = index; + ImGui::SameLine(); + 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::PopStyleColor(); + ImGui::PopID(); + } + ImGui::EndChild(); + if (unbind) + infra_unbind({*unbind}); + ImGui::TreePop(); + } +} + +void editor_mode::draw_infra_overlay() const +{ + auto const &state{m_infra}; + if (false == state.open || false == state.show) + return; + Live.refresh(); + screen_projection const projection; + ImDrawList *drawlist{ImGui::GetBackgroundDrawList()}; + auto const camera{GfxRenderer->Camera_Position()}; + auto const draw = [&](infra::binding const &Binding, ImU32 const Colour, float const Size) { + if (Binding.anchors.empty() || glm::distance(Binding.anchors.front().foot, camera) > 2000.0) + return; + std::vector points; + std::optional yaw; + if (false == alive(Binding) || false == object_points(Binding, points, yaw)) + return; + for (std::size_t i = 0; i < points.size() && i < Binding.anchors.size(); ++i) + { + auto anchor{Binding.anchors[i]}; + auto const foot{infra::frame_at(anchor.at).point}; + projection.line(drawlist, foot, points[i], Colour, 1.5f); + } + ImVec2 screen; + if (projection.project(points.front(), screen)) + drawlist->AddCircleFilled(screen, Size, Colour); + }; + for (auto const &binding : m_bindings) + draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); + for (int i = 0; i < static_cast(state.candidates.size()); ++i) + { + auto const &candidate{state.candidates[i]}; + if (candidate.bound) + continue; + draw(candidate.binding, colour(candidate.binding.group, candidate.chosen ? 255 : 110), i == state.hovered ? 9.f : (candidate.chosen ? 5.f : 3.f)); + } +} diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp new file mode 100644 index 00000000..43a57374 --- /dev/null +++ b/application/editormodeprofile.cpp @@ -0,0 +1,1237 @@ +/* +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/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "world/Track.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::plan_distance; + +ImU32 const kGradeLine{IM_COL32(90, 230, 110, 255)}; +ImU32 const kTangents{IM_COL32(255, 210, 60, 130)}; +ImU32 const kTrack{IM_COL32(190, 190, 190, 255)}; +ImU32 const kTerrain{IM_COL32(170, 120, 70, 255)}; +ImU32 const kGrid{IM_COL32(255, 255, 255, 22)}; +ImU32 const kLabel{IM_COL32(200, 200, 200, 200)}; +ImU32 const kSwitch{IM_COL32(160, 100, 220, 45)}; +ImU32 const kError{IM_COL32(255, 80, 70, 255)}; +ImU32 const kWarning{IM_COL32(255, 200, 60, 255)}; + +double nice_step(double const Approximate) +{ + auto const power{std::pow(10.0, std::floor(std::log10(std::max(Approximate, 1e-6))))}; + for (auto const factor : {1.0, 2.0, 5.0, 10.0}) + if (power * factor >= Approximate) + return power * factor; + return power * 10.0; +} + +double route_speed(editor_track::route const &Route) +{ + double speed{-1.0}; + for (auto const &span : Route.spans) + speed = std::max(speed, editor_track::velocity(*span.track)); + return speed > 0.0 ? speed : 100.0; +} + +std::string const kProfileMark{"//$p"}; + +profile::line reversed_line(profile::line Line, double const Length) +{ + std::reverse(Line.points.begin(), Line.points.end()); + for (auto &point : Line.points) + point.chainage = Length - point.chainage; + std::swap(Line.join_start, Line.join_end); + return Line; +} + +bool end_locked(profile::line const &Line, profile::context const &Context, int const Index) +{ + if (Index == 0) + return Line.join_start && Context.start_joined; + if (Index + 1 == static_cast(Line.points.size())) + return Line.join_end && Context.end_joined; + return false; +} + +bool has_errors(std::vector const &Issues) +{ + return std::any_of(Issues.begin(), Issues.end(), [](profile::issue const &Issue) { return Issue.error; }); +} + +} + +// format of the lines, coordinates in the space of the file: +// profile +// speed origin join +// norms = k*V^2> +// point +// end +void editor_mode::profile_library_load() +{ + if (m_profiles_loaded) + return; + m_profiles_loaded = true; + m_profiles.clear(); + for (auto const layer : scene::Layers.marked_layers(kProfileMark)) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const height = [&](double const Local) { return context.to_world({0.0, Local, 0.0}).y; }; + std::optional current; + for (auto const &text : scene::Layers.marked(layer, kProfileMark)) + { + std::istringstream line{text}; + std::string keyword; + line >> keyword; + if (keyword == "profile") + { + stored_profile entry; + entry.layer = layer; + glm::dvec3 start, end, middle; + line >> entry.length >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z >> middle.x >> middle.y >> middle.z; + if (line.fail()) + { + current.reset(); + continue; + } + entry.start = context.to_world(start); + entry.end = context.to_world(end); + entry.middle = context.to_world(middle); + current = entry; + continue; + } + if (false == current.has_value()) + continue; + auto &entry{*current}; + std::string word; + if (keyword == "speed") + { + int start{1}, end{1}; + line >> entry.line.speed >> word >> entry.origin >> word >> start >> end; + entry.line.join_start = (start != 0); + entry.line.join_end = (end != 0); + } + else if (keyword == "norms") + { + auto &norms{entry.line.norms}; + line >> norms.grade_max >> norms.grade_switch_max >> norms.radius_factor >> norms.radius_min >> norms.curve_threshold >> norms.element_min >> norms.fixed_tolerance; + } + else if (keyword == "point") + { + profile::pvi point; + int automatic{0}; + line >> point.chainage >> point.elevation >> point.radius >> automatic; + if (line.fail()) + continue; + point.elevation = height(point.elevation); + point.automatic = (automatic != 0); + entry.line.points.push_back(point); + } + else if (keyword == "end") + { + if (entry.line.points.size() >= 2 && entry.length > 0.0) + m_profiles.push_back(entry); + current.reset(); + } + } + } + if (false == m_profiles.empty()) + WriteLog("Editor: " + std::to_string(m_profiles.size()) + " vertical profile design(s) read from the scenery", logtype::generic); +} + +void editor_mode::profile_library_mark(std::vector Layers) +{ + std::sort(Layers.begin(), Layers.end()); + Layers.erase(std::unique(Layers.begin(), Layers.end()), Layers.end()); + for (auto const layer : Layers) + { + if (false == scene::Layers.valid(layer)) + continue; + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + std::vector lines; + for (auto const &entry : m_profiles) + { + if (entry.layer != layer) + continue; + if (lines.empty()) + lines.emplace_back("# vertical profiles (grade lines) designed in the track editor, the simulation skips these lines"); + lines.push_back(format("profile %.4f ", entry.length) + local(entry.start) + ' ' + local(entry.end) + ' ' + local(entry.middle)); + lines.push_back(format("speed %.1f origin %.4f join %d %d", entry.line.speed, entry.origin, entry.line.join_start ? 1 : 0, entry.line.join_end ? 1 : 0)); + auto const &norms{entry.line.norms}; + lines.push_back(format("norms %.4f %.4f %.4f %.1f %.4f %.2f %.4f", norms.grade_max, norms.grade_switch_max, norms.radius_factor, norms.radius_min, norms.curve_threshold, norms.element_min, norms.fixed_tolerance)); + for (auto const &point : entry.line.points) + lines.push_back(format("point %.4f %.4f %.1f %d", point.chainage, context.to_local({0.0, point.elevation, 0.0}).y, point.radius, point.automatic ? 1 : 0)); + lines.emplace_back("end"); + } + scene::Layers.mark(layer, kProfileMark, std::move(lines)); + } +} + +int editor_mode::profile_find(bool &Reversed) const +{ + auto const &state{m_profile}; + Reversed = false; + if (state.samples.empty()) + return -1; + auto const start{state.samples.front().position}; + auto const end{state.samples.back().position}; + auto const middle{editor_track::sampled_position(state.samples, state.route.length * 0.5)}; + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + if (std::abs(entry.length - state.route.length) > 0.5 || plan_distance(entry.middle, middle) > 0.5) + continue; + if (plan_distance(entry.start, start) <= 0.1 && plan_distance(entry.end, end) <= 0.1) + return static_cast(i); + if (plan_distance(entry.start, end) <= 0.1 && plan_distance(entry.end, start) <= 0.1) + { + Reversed = true; + return static_cast(i); + } + } + return -1; +} + +bool editor_mode::profile_restore() +{ + profile_library_load(); + auto &state{m_profile}; + bool reversed; + auto const index{profile_find(reversed)}; + if (index < 0) + return false; + auto const &entry{m_profiles[index]}; + state.line = reversed ? reversed_line(entry.line, entry.length) : entry.line; + state.origin = entry.origin; + state.selected = -1; + state.changed = false; + 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); + return true; +} + +void editor_mode::profile_store() +{ + auto &state{m_profile}; + if (state.route.spans.empty() || state.samples.empty() || state.line.points.size() < 2) + return; + if (scene::Layers.empty()) + { + state.error = "The scenery wasn't opened for editing, the design can't be kept in its files"; + return; + } + profile_library_load(); + stored_profile entry; + entry.length = state.route.length; + entry.start = state.samples.front().position; + entry.end = state.samples.back().position; + entry.middle = editor_track::sampled_position(state.samples, state.route.length * 0.5); + entry.origin = state.origin; + entry.line = state.line; + auto &points{entry.line.points}; + points.erase(std::remove_if(points.begin(), points.end(), [](profile::pvi const &Point) { return Point.joint; }), points.end()); + + bool reversed; + auto const index{profile_find(reversed)}; + if (index >= 0) + { + entry.layer = m_profiles[index].layer; + m_profiles[index] = entry; + } + else + { + auto layer{scene::Layers.resolve(state.route.spans.front().track->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + entry.layer = layer; + m_profiles.push_back(entry); + } + profile_library_mark({entry.layer}); + state.changed = false; +} + +void editor_mode::profile_forget() +{ + auto &state{m_profile}; + profile_library_load(); + bool reversed; + auto const index{profile_find(reversed)}; + if (index < 0) + return; + auto const layer{m_profiles[index].layer}; + m_profiles.erase(m_profiles.begin() + index); + profile_library_mark({layer}); + state.status = "The design will be removed from " + scene::Layers.layer(layer).name + " on save"; +} + +void editor_mode::profile_open_stored(std::size_t const Index) +{ + auto &state{m_profile}; + if (Index >= m_profiles.size()) + return; + auto const entry{m_profiles[Index]}; + auto const touching = [](glm::dvec3 const &Point) { + std::vector result; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == editor_track::is_supported(*track)) + continue; + for (auto const &path : track->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + if (plan_distance(path.points[index], Point) <= 0.1 && std::abs(path.points[index].y - Point.y) < 3.0) + { + if (std::find(result.begin(), result.end(), track) == result.end()) + result.push_back(track); + } + } + return result; + }; + for (auto *from : touching(entry.start)) + for (auto *to : touching(entry.end)) + { + editor_track::route route; + std::string error; + if (false == editor_track::find_route(from, to, route, error) || std::abs(route.length - entry.length) > 0.5) + continue; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.empty() || plan_distance(samples.front().position, entry.start) > 0.1 || plan_distance(editor_track::sampled_position(samples, route.length * 0.5), entry.middle) > 0.5) + continue; + profile_open(from, to); + return; + } + state.open = true; + state.error = "The route of the stored design isn't in the scenery any more (paths moved or removed)"; +} + +void editor_mode::profile_edited() +{ + m_profile.changed = true; + profile_check(); +} + +void editor_mode::profile_open(TTrack *From, TTrack *To) +{ + auto &state{m_profile}; + state.open = true; + state.status.clear(); + editor_track::route route; + if (false == editor_track::find_route(From, To, route, state.error)) + return; + profile_take(std::move(route), From, To); +} + +void editor_mode::profile_open_run(TTrack &Track) +{ + auto &state{m_profile}; + state.open = true; + state.status.clear(); + state.error.clear(); + auto route{editor_track::run_route(Track, state.run_length)}; + if (route.spans.empty()) + { + state.error = "The path can't carry a profile"; + return; + } + auto *from{route.spans.front().track}; + auto *to{route.spans.back().track}; + profile_take(std::move(route), from, to); +} + +void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack *To) +{ + auto &state{m_profile}; + state.from = state.picked_from = From; + state.to = state.picked_to = To; + state.route = std::move(Route); + state.selected = -1; + profile_resample(); + if (false == profile_restore()) + profile_recognize(); + profile_fit_view(); +} + +void editor_mode::profile_resample() +{ + auto &state{m_profile}; + state.samples = editor_track::sample_route(state.route, 1.0); + auto const &samples{state.samples}; + state.terrain.assign(samples.size(), std::numeric_limits::quiet_NaN()); + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const &position{samples[i].position}; + 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}; + context = {}; + context.length = state.route.length; + if (samples.empty()) + return; + for (auto const &span : state.route.spans) + if (span.track->eType != tt_Normal) + context.switches.push_back({span.from, span.to, span.track->name()}); + context.start_elevation = samples.front().position.y; + context.end_elevation = samples.back().position.y; + context.start_joined = editor_track::adjoining_grade(state.route, false, context.start_grade); + context.end_joined = editor_track::adjoining_grade(state.route, true, context.end_grade); + profile_check(); +} + +void editor_mode::profile_recognize() +{ + auto &state{m_profile}; + std::vector chainages, elevations, grades; + for (auto const &sample : state.samples) + { + chainages.push_back(sample.chainage); + elevations.push_back(sample.position.y); + grades.push_back(sample.grade); + } + auto const norms{state.line.norms}; + state.line = profile::recognize(chainages, elevations, grades, {}); + state.line.norms = norms; + state.line.speed = route_speed(state.route); + state.selected = -1; + profile_check(); + state.status = format("Grade line recognized from the track: %zu points, largest departure %.3f m", state.line.points.size(), state.departure); +} + +void editor_mode::profile_check() +{ + auto &state{m_profile}; + auto &points{state.line.points}; + // joint points come and go, the selection follows the point it was made on + struct mark + { + int *index; + double chainage; + bool joint; + }; + std::vector marks; + for (auto *index : {&state.selected, &state.dragging, &state.curve_grip}) + if (*index >= 0 && *index < static_cast(points.size())) + marks.push_back({index, points[*index].chainage, points[*index].joint}); + profile::complete(state.line, state.context); + for (auto const &mark : marks) + { + int best{-1}; + double distance{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(points.size()); ++i) + if (points[i].joint == mark.joint && std::abs(points[i].chainage - mark.chainage) < distance) + { + distance = std::abs(points[i].chainage - mark.chainage); + best = i; + } + *mark.index = (mark.joint || distance < 1e-6) ? best : -1; + } + state.issues = profile::check(state.line, state.context); + state.departure = 0.0; + for (auto const &sample : state.samples) + state.departure = std::max(state.departure, std::abs(profile::elevation(state.line, sample.chainage) - sample.position.y)); +} + +void editor_mode::profile_fit_view() +{ + auto &state{m_profile}; + auto const length{std::max(1.0, state.route.length)}; + state.view_from = -0.02 * length; + state.view_to = length * 1.02; + state.exaggeration = 0.0f; +} + +void editor_mode::profile_apply() +{ + auto &state{m_profile}; + state.status.clear(); + state.error.clear(); + if (state.route.spans.empty() || state.line.points.size() < 2) + return; + if (has_errors(state.issues)) + { + state.error = "Correct the errors to apply the grade line"; + return; + } + auto const line{state.line}; + std::vector> states; + std::vector created; + auto const gaps{editor_track::apply_profile( + 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 added{created.size()}; + push_track_snapshot(std::move(states), std::move(created)); + state.from = state.route.spans.front().track; + state.to = state.route.spans.back().track; + profile_resample(); + profile_store(); + state.status = format("Grade line applied: %zu paths changed, %zu added, largest departure %.3f m", changed, added, state.departure); + if (false == scene::Layers.empty()) + state.status += "\nThe paths and the design (//$p lines) go to the scenery files on save"; + if (false == gaps.empty()) + state.status += "\nSwitch ends off the adjoining paths, to be corrected:"; + 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); + WriteLog("Editor: vertical profile - " + state.status, logtype::generic); +} + +void editor_mode::profile_after_undo() +{ + auto &state{m_profile}; + if (state.route.spans.empty()) + return; + auto const removed = [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }; + if (removed(state.from) || removed(state.to)) + { + state.from = state.picked_from; + state.to = state.picked_to; + } + std::string error; + if (removed(state.from) || removed(state.to) || false == editor_track::find_route(state.from, state.to, state.route, error)) + { + state.route = {}; + state.samples.clear(); + state.selected = state.dragging = state.curve_grip = -1; + return; + } + profile_resample(); +} + +void editor_mode::render_profile_window() +{ + 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(); + 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)"); + if (state.picked_from != nullptr) + ImGui::Text("Start: %s", state.picked_from->name().c_str()); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + ImGui::End(); + 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()); + { + bool reversed; + auto const stored{profile_find(reversed)}; + 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::SameLine(); + if (ImGui::SmallButton("Forget the design")) + profile_forget(); + } + else if (state.changed) + ImGui::TextDisabled("The design goes to the scenery file of the route on save"); + } + + if (render_profile_parameters()) + profile_edited(); + + render_profile_canvas(); + + ImGui::Columns(2, "##profilecolumns", true); + render_profile_point(); + ImGui::NextColumn(); + render_profile_issues(); + ImGui::Columns(1); + ImGui::End(); +} + +void editor_mode::render_profile_source() +{ + auto &state{m_profile}; + auto *selected{selected_track()}; + if (ImGui::Button("Along the line from the selected path") && selected != nullptr) + profile_open_run(*selected); + ImGui::SameLine(); + ImGui::PushItemWidth(70.0f); + ImGui::InputDouble("m each way##profilerun", &state.run_length, 0.0, 0.0, "%.0f"); + ImGui::PopItemWidth(); + state.run_length = std::clamp(state.run_length, 10.0, 50000.0); + ImGui::SameLine(); + if (ImGui::Button("Start: selected path") && selected != nullptr) + state.picked_from = selected; + ImGui::SameLine(); + if (ImGui::Button("End: selected path") && selected != nullptr && state.picked_from != nullptr) + profile_open(state.picked_from, selected); + profile_library_load(); + if (false == m_profiles.empty() && ImGui::TreeNode("##profilestored", "Designs kept in the scenery (%zu)", m_profiles.size())) + { + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + ImGui::PushID(static_cast(i)); + if (ImGui::SmallButton("Open")) + profile_open_stored(i); + 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::PopID(); + } + ImGui::TreePop(); + } +} + +bool editor_mode::render_profile_parameters() +{ + auto &state{m_profile}; + auto &line{state.line}; + bool changed{false}; + ImGui::PushItemWidth(90.0f); + ImGui::InputDouble("Chainage of the start (m)", &state.origin, 0.0, 0.0, "%.2f"); + ImGui::SameLine(); + changed |= ImGui::InputDouble("Design speed (km/h)", &line.speed, 0.0, 0.0, "%.0f"); + ImGui::SameLine(); + ImGui::SliderFloat("Exaggeration##profile", &state.exaggeration, 1.0f, 500.0f, "1:%.0f", 2.0f); + ImGui::PopItemWidth(); + line.speed = std::max(1.0, line.speed); + ImGui::Checkbox("Track", &state.show_track); + ImGui::SameLine(); + ImGui::Checkbox("Terrain", &state.show_terrain); + ImGui::SameLine(); + ImGui::Checkbox("Plan curvature", &state.show_plan); + ImGui::SameLine(); + if (ImGui::Button("Fit the view")) + profile_fit_view(); + ImGui::SameLine(); + if (ImGui::Button("Recognize from the track")) + { + profile_recognize(); + state.changed = true; + } + ImGui::SameLine(); + if (ImGui::Button("Apply to the scenery")) + profile_apply(); + if (ImGui::TreeNode("Limits##profile")) + { + auto &norms{line.norms}; + ImGui::PushItemWidth(90.0f); + changed |= ImGui::InputDouble("Maximum grade (per mille)", &norms.grade_max, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Maximum grade over switches (per mille)", &norms.grade_switch_max, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.radius_factor, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.radius_min, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Vertical curve needed above a change of (per mille)", &norms.curve_threshold, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble("Constant grade between curves at least (m)", &norms.element_min, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble("Tolerance at fixed points (m)", &norms.fixed_tolerance, 0.0, 0.0, "%.3f"); + ImGui::PopItemWidth(); + if (ImGui::SmallButton("Automatic R at all points")) + { + for (auto &point : line.points) + point.automatic = true; + changed = true; + } + ImGui::TextDisabled("Required R for %.0f km/h: %.0f m", line.speed, profile::required_radius(line)); + ImGui::TreePop(); + } + if (state.context.start_joined) + changed |= ImGui::Checkbox("Join the adjoining track at the start", &line.join_start); + if (state.context.start_joined && state.context.end_joined) + ImGui::SameLine(); + if (state.context.end_joined) + changed |= ImGui::Checkbox("Join the adjoining track at the end", &line.join_end); + if ((state.context.start_joined || state.context.end_joined) && ImGui::IsItemHovered()) + ImGui::SetTooltip("Keeps the elevation of the joint and takes over the grade of the adjoining track,\n" + "with a vertical curve which starts right at the joint"); + return changed; +} + +void editor_mode::render_profile_issues() +{ + auto &state{m_profile}; + ImGui::Text("Largest departure from the track: %.3f m", state.departure); + ImGui::BeginChild("##profileissues", ImVec2(0.0f, 0.0f), false); + if (state.issues.empty()) + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "The grade line meets the limits"); + for (std::size_t i = 0; i < state.issues.size(); ++i) + { + auto const &issue{state.issues[i]}; + ImGui::PushID(static_cast(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)); + if (ImGui::Selectable(issue.text.c_str())) + { + if (issue.point >= 0) + state.selected = issue.point; + auto const half{(state.view_to - state.view_from) * 0.5}; + state.view_from = issue.chainage - half; + state.view_to = issue.chainage + half; + } + ImGui::PopStyleColor(); + ImGui::PopID(); + } + if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + ImGui::EndChild(); +} + +void editor_mode::render_profile_point() +{ + auto &state{m_profile}; + auto &points{state.line.points}; + bool changed{false}; + auto const count{static_cast(points.size())}; + if (state.selected >= count) + state.selected = -1; + if (state.selected < 0) + { + ImGui::TextDisabled("LMB: select and drag a point (Ctrl: height only, Shift: keep the grade before)\n" + "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) + { + auto const index{state.selected}; + auto const &point{points[index]}; + 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("km %s H %.3f R %.0f", 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::TextDisabled("Moves with the neighbouring point; untick the joining to edit the end freely"); + } + else + { + auto const index{state.selected}; + auto &point{points[index]}; + bool const end{index == 0 || index == count - 1}; + 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::PushItemWidth(110.0f); + auto chainage{point.chainage + state.origin}; + if (ImGui::InputDouble("Chainage (m)", &chainage, 0.0, 0.0, "%.2f") && false == end) + { + point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + changed = true; + } + ImGui::SameLine(); + ImGui::TextDisabled("km %s", profile::format_chainage(point.chainage + state.origin).c_str()); + changed |= ImGui::InputDouble("Elevation (m)", &point.elevation, 0.0, 0.0, "%.3f", locked ? ImGuiInputTextFlags_ReadOnly : 0); + if (index > 0 && false == locked) + { + 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")) + { + point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage); + changed = true; + } + } + if (index + 1 < count) + { + 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")) + { + points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage); + changed = true; + } + } + if (false == end) + { + if (ImGui::InputDouble("Vertical curve R (m, 0: none)", &point.radius, 0.0, 0.0, "%.0f")) + { + point.automatic = false; + changed = true; + } + point.radius = std::max(0.0, point.radius); + ImGui::SameLine(); + changed |= ImGui::Checkbox("Auto", &point.automatic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("R follows the limits: the required radius where the change of grade needs a curve"); + 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}; + ImGui::Text("Change of grade %+.3f per mille, %s", change, change > 0.0 ? "sag" : "crest"); + if (point.radius > 0.0) + ImGui::Text("Tangent T %.2f m, sagitta f %.3f m", tangent, tangent * tangent / (2.0 * point.radius)); + } + ImGui::PopItemWidth(); + if (index + 1 < count && ImGui::SmallButton("Insert a point halfway to the next")) + { + profile::pvi inserted; + inserted.chainage = (point.chainage + points[index + 1].chainage) * 0.5; + inserted.elevation = (point.elevation + points[index + 1].elevation) * 0.5; + points.insert(points.begin() + index + 1, inserted); + state.selected = index + 1; + changed = true; + } + if (false == end) + { + ImGui::SameLine(); + if (ImGui::SmallButton("Remove the point")) + { + points.erase(points.begin() + index); + state.selected = -1; + changed = true; + } + } + } + ImGui::BeginChild("##profilepoints", ImVec2(0.0f, 0.0f), true); + for (int i = 0; i < static_cast(points.size()); ++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(label.c_str(), state.selected == i)) + state.selected = (state.selected == i ? -1 : i); + } + ImGui::EndChild(); + if (changed) + profile_edited(); +} + +// screen mapping of the profile canvas, chainage across and elevation upwards +struct editor_mode::profile_view +{ + ImVec2 corner; + ImVec2 size; + float strip; + float top; + float height; + double across; + double vertical; + double from; + double centre; + + profile_view(profile_state const &State, ImVec2 const Corner, ImVec2 const Size) : + corner{Corner}, size{Size}, strip{State.show_plan ? 44.0f : 0.0f}, top{18.0f}, height{Size.y - strip - top}, + across{std::max(1.0, State.view_to - State.view_from) / Size.x}, vertical{across / State.exaggeration}, from{State.view_from}, centre{State.view_centre} + { + } + float x_of(double const Chainage) const { return corner.x + static_cast((Chainage - from) / across); } + float y_of(double const Height) const { return corner.y + top + height * 0.5f - static_cast((Height - centre) / vertical); } + double chainage_of(float const X) const { return from + (X - corner.x) * across; } + double height_of(float const Y) const { return centre - (Y - corner.y - top - height * 0.5f) * vertical; } + float plot_top() const { return corner.y + top; } + float plot_bottom() const { return corner.y + top + height; } + ImVec2 bottom_right() const { return {corner.x + size.x, corner.y + size.y}; } + float mouse_distance2(double const Chainage, double const Height) const + { + auto const &mouse{ImGui::GetIO().MousePos}; + auto const dx{x_of(Chainage) - mouse.x}; + auto const dy{y_of(Height) - mouse.y}; + return dx * dx + dy * dy; + } +}; + +void editor_mode::render_profile_canvas() +{ + auto &state{m_profile}; + auto const available{ImGui::GetContentRegionAvail()}; + ImVec2 const size{std::max(200.0f, available.x), std::max(240.0f, available.y * 0.6f)}; + ImVec2 const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##profilecanvas", size); + bool const hovered{ImGui::IsItemHovered()}; + + if (state.exaggeration <= 0.0f) + profile_fit_exaggeration(profile_view{state, corner, size}); + profile_navigate(profile_view{state, corner, size}, hovered); + profile_view const view{state, corner, size}; + if (profile_canvas_edit(view, hovered)) + profile_edited(); + state.hover = hovered ? view.chainage_of(ImGui::GetIO().MousePos.x) : -1.0; + + auto *draw{ImGui::GetWindowDrawList()}; + draw->AddRectFilled(corner, view.bottom_right(), IM_COL32(22, 24, 28, 255)); + draw->PushClipRect(corner, view.bottom_right(), true); + draw_profile_canvas(view, *draw); + if (hovered && state.hover >= 0.0 && state.hover <= state.route.length && false == state.samples.empty()) + { + 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)); + profile_canvas_tooltip(); + } + draw->PopClipRect(); +} + +void editor_mode::profile_fit_exaggeration(profile_view const &View) +{ + auto &state{m_profile}; + double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; + for (auto const &sample : state.samples) + { + low = std::min(low, sample.position.y); + high = std::max(high, sample.position.y); + } + for (auto const &point : state.line.points) + { + low = std::min(low, point.elevation); + high = std::max(high, point.elevation); + } + if (low > high) + low = high = 0.0; + auto const range{std::max(1.0, (high - low) * 1.3)}; + state.exaggeration = static_cast(std::clamp(View.across / (range / View.height), 1.0, 500.0)); + state.view_centre = (low + high) * 0.5; +} + +void editor_mode::profile_navigate(profile_view const &View, bool const Hovered) +{ + auto &state{m_profile}; + ImGuiIO const &io{ImGui::GetIO()}; + if (Hovered && io.MouseWheel != 0.0f) + { + if (io.KeyCtrl) + state.exaggeration = std::clamp(state.exaggeration * std::pow(1.2f, io.MouseWheel), 1.0f, 500.0f); + else + { + auto const anchor{View.chainage_of(io.MousePos.x)}; + auto const factor{std::pow(0.8, static_cast(io.MouseWheel))}; + state.view_from = anchor - (anchor - state.view_from) * factor; + state.view_to = anchor + (state.view_to - anchor) * factor; + } + } + if (Hovered && (ImGui::IsMouseClicked(1) || ImGui::IsMouseClicked(2))) + state.panning = true; + if (state.panning) + { + if (ImGui::IsMouseDown(1) || ImGui::IsMouseDown(2)) + { + state.view_from -= io.MouseDelta.x * View.across; + state.view_to -= io.MouseDelta.x * View.across; + state.view_centre += io.MouseDelta.y * View.vertical; + } + else + state.panning = false; + } +} + +bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hovered) +{ + auto &state{m_profile}; + auto &line{state.line}; + auto &points{line.points}; + ImGuiIO const &io{ImGui::GetIO()}; + ImVec2 const mouse{io.MousePos}; + + auto const hit_point = [&]() { + int hit{-1}; + float best{64.0f}; + for (int i = 0; i < static_cast(points.size()); ++i) + { + if (points[i].joint) + continue; + if (auto const distance{View.mouse_distance2(points[i].chainage, points[i].elevation)}; distance < best) + { + best = distance; + hit = i; + } + } + return hit; + }; + auto const hit_grip = [&]() { + for (auto const &c : profile::curves(line)) + { + if (points[c.point].joint) + continue; + auto const chainage{points[c.point].chainage}; + if (View.mouse_distance2(chainage, profile::elevation(line, chainage)) < 64.0f) + return static_cast(c.point); + } + return -1; + }; + bool changed{false}; + if (Hovered && ImGui::IsMouseClicked(0)) + { + auto const point{hit_point()}; + auto const grip{point < 0 ? hit_grip() : -1}; + if (point >= 0) + state.selected = state.dragging = point; + else if (grip >= 0) + state.selected = state.curve_grip = grip; + else + state.selected = -1; + } + if (Hovered && ImGui::IsMouseDoubleClicked(0) && state.dragging < 0 && state.curve_grip < 0 && points.size() >= 2) + { + auto const chainage{View.chainage_of(mouse.x)}; + if (chainage > points.front().chainage + 0.5 && chainage < points.back().chainage - 0.5 && std::abs(View.y_of(profile::elevation(line, chainage)) - mouse.y) < 12.0f) + { + std::size_t index{0}; + while (index + 2 < points.size() && points[index + 1].chainage < chainage) + ++index; + profile::pvi inserted; + inserted.chainage = std::round(chainage * 100.0) / 100.0; + inserted.elevation = points[index].elevation + profile::grade_after(line, index) * (inserted.chainage - points[index].chainage); + points.insert(points.begin() + index + 1, inserted); + state.selected = static_cast(index) + 1; + changed = true; + } + } + if (state.dragging >= 0 && state.dragging < static_cast(points.size())) + { + if (ImGui::IsMouseDown(0)) + { + auto const index{state.dragging}; + auto &point{points[index]}; + bool const end{index == 0 || index + 1 == static_cast(points.size())}; + auto const incoming{index > 0 ? profile::grade_after(line, index - 1) : 0.0}; + if (false == end && false == io.KeyCtrl) + point.chainage = std::clamp(std::round(View.chainage_of(mouse.x) * 100.0) / 100.0, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + if (end_locked(line, state.context, index)) + ; + else if (io.KeyShift && index > 0) + point.elevation = points[index - 1].elevation + incoming * (point.chainage - points[index - 1].chainage); + else + point.elevation = std::round(View.height_of(mouse.y) * 1000.0) / 1000.0; + changed = true; + } + else + state.dragging = -1; + } + else + state.dragging = -1; + if (state.curve_grip > 0 && state.curve_grip + 1 < static_cast(points.size())) + { + if (ImGui::IsMouseDown(0)) + { + auto const sagitta{View.height_of(mouse.y) - points[state.curve_grip].elevation}; + points[state.curve_grip].radius = std::max(10.0, std::round(profile::radius_for_sagitta(line, state.curve_grip, sagitta) / 10.0) * 10.0); + points[state.curve_grip].automatic = false; + changed = true; + } + else + state.curve_grip = -1; + } + else + state.curve_grip = -1; + if ((Hovered || ImGui::IsWindowFocused()) && state.selected > 0 && state.selected + 1 < static_cast(points.size()) && false == points[state.selected].joint && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Delete))) + { + points.erase(points.begin() + state.selected); + state.selected = -1; + changed = true; + } + return changed; +} + +void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw) const +{ + auto const &state{m_profile}; + auto const &line{state.line}; + auto const &points{line.points}; + auto const &samples{state.samples}; + auto const bottomright{View.bottom_right()}; + auto const plottop{View.plot_top()}; + auto const plotbottom{View.plot_bottom()}; + + auto const step{nice_step(View.across * 110.0)}; + for (auto s = std::ceil((state.view_from + state.origin) / step) * step - state.origin; s <= state.view_to; s += step) + { + auto const x{View.x_of(s)}; + 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()); + } + 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) + { + auto const y{View.y_of(h)}; + 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) + { + 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()); + } + + auto const polyline = [&](auto const &Height, ImU32 const Colour, float const Thickness) { + ImVec2 previous{}; + bool open{false}; + float lastx{-1e9f}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{View.x_of(samples[i].chainage)}; + auto const h{Height(i)}; + if (std::isnan(h) || x < View.corner.x - 50.0f || x > bottomright.x + 50.0f) + { + open = false; + continue; + } + if (open && x - lastx < 1.0f && i + 1 < samples.size()) + continue; + ImVec2 const next{x, View.y_of(h)}; + if (open) + Draw.AddLine(previous, next, Colour, Thickness); + previous = next; + lastx = x; + open = true; + } + }; + if (state.show_terrain) + polyline([&](std::size_t const I) { return state.terrain[I]; }, kTerrain, 1.5f); + if (state.show_track) + polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f); + + for (std::size_t i = 0; i + 1 < points.size(); ++i) + Draw.AddLine(ImVec2(View.x_of(points[i].chainage), View.y_of(points[i].elevation)), ImVec2(View.x_of(points[i + 1].chainage), View.y_of(points[i + 1].elevation)), kTangents, 1.0f); + { + auto const from{std::max(points.front().chainage, View.chainage_of(View.corner.x))}; + auto const to{std::min(points.back().chainage, View.chainage_of(bottomright.x))}; + ImVec2 previous{View.x_of(from), View.y_of(profile::elevation(line, from))}; + for (auto x = previous.x + 2.0f; x <= View.x_of(to) + 2.0f; x += 2.0f) + { + auto const chainage{std::min(to, View.chainage_of(x))}; + ImVec2 const next{View.x_of(chainage), View.y_of(profile::elevation(line, chainage))}; + Draw.AddLine(previous, next, kGradeLine, 2.5f); + previous = next; + } + } + for (std::size_t i = 0; i + 1 < points.size(); ++i) + { + auto const middle{(points[i].chainage + points[i + 1].chainage) * 0.5}; + auto const x{View.x_of(middle)}; + if (View.x_of(points[i + 1].chainage) - View.x_of(points[i].chainage) < 70.0f) + continue; + auto const text{format("%+.2f o/oo L %.1f", 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()); + } + for (auto const &c : profile::curves(line)) + { + for (auto const at : {c.start, c.end}) + { + auto const y{View.y_of(profile::elevation(line, at))}; + Draw.AddLine(ImVec2(View.x_of(at), y - 7.0f), ImVec2(View.x_of(at), y + 7.0f), kGradeLine, 1.5f); + } + auto const chainage{points[c.point].chainage}; + ImVec2 const grip{View.x_of(chainage), View.y_of(profile::elevation(line, chainage))}; + Draw.AddCircleFilled(grip, 4.5f, overlay_color::grip, 4); + if (static_cast(c.point) == state.curve_grip) + Draw.AddCircle(grip, 9.0f, overlay_color::highlight, 12, 2.0f); + Draw.AddText(ImVec2(grip.x + 7.0f, grip.y + 4.0f), overlay_color::grip, format("R %.0f", points[c.point].radius).c_str()); + } + for (int i = 0; i < static_cast(points.size()); ++i) + { + ImVec2 const at{View.x_of(points[i].chainage), View.y_of(points[i].elevation)}; + if (i == 0 || i + 1 == static_cast(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)); + 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)); + else + Draw.AddCircleFilled(at, 5.5f, overlay_color::marked); + if (i == state.selected) + Draw.AddCircle(at, 10.0f, overlay_color::highlight, 16, 2.0f); + Draw.AddText(ImVec2(at.x + 7.0f, at.y - 18.0f), overlay_color::marked, format("%d %.3f", i + 1, points[i].elevation).c_str()); + } + for (auto const &issue : state.issues) + { + auto const x{View.x_of(issue.chainage)}; + Draw.AddTriangleFilled(ImVec2(x - 5.0f, plottop), ImVec2(x + 5.0f, plottop), ImVec2(x, plottop + 8.0f), issue.error ? kError : kWarning); + } + if (state.show_plan && false == samples.empty()) + { + auto const middle{plotbottom + View.strip * 0.5f}; + Draw.AddLine(ImVec2(View.corner.x, plotbottom), ImVec2(bottomright.x, plotbottom), IM_COL32(255, 255, 255, 60)); + Draw.AddLine(ImVec2(View.corner.x, middle), ImVec2(bottomright.x, middle), IM_COL32(255, 255, 255, 40)); + ImVec2 previous{}; + bool open{false}; + float lastx{-1e9f}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{View.x_of(samples[i].chainage)}; + if (x < View.corner.x - 50.0f || x > bottomright.x + 50.0f || (open && x - lastx < 1.0f)) + continue; + ImVec2 const next{x, middle - std::clamp(static_cast(samples[i].curvature * 300.0 * View.strip * 0.4), -View.strip * 0.45f, View.strip * 0.45f)}; + if (open) + Draw.AddLine(previous, next, IM_COL32(120, 190, 255, 255), 1.5f); + previous = next; + lastx = x; + open = true; + } + Draw.AddText(ImVec2(View.corner.x + 2.0f, plotbottom + 1.0f), kLabel, "plan: curvature (up: left)"); + } +} + +void editor_mode::profile_canvas_tooltip() const +{ + auto const &state{m_profile}; + auto const &samples{state.samples}; + auto const designed{profile::elevation(state.line, state.hover)}; + auto const existing{editor_track::sampled_elevation(samples, state.hover)}; + ImGui::BeginTooltip(); + ImGui::Text("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::Text("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 index{static_cast(std::distance(samples.begin(), std::min(next, std::prev(samples.end()))))}; + 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])); + 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::EndTooltip(); +} + +void editor_mode::draw_profile_overlay() const +{ + auto const &state{m_profile}; + if (false == state.open || state.samples.size() < 2) + return; + screen_projection const projection; + ImDrawList *drawlist{ImGui::GetBackgroundDrawList()}; + auto const &samples{state.samples}; + glm::dvec3 previous{samples.front().position}; + glm::dvec3 designedprevious{previous.x, profile::elevation(state.line, 0.0), previous.z}; + for (std::size_t i = 1; i < samples.size(); ++i) + { + if (glm::distance(samples[i].position, previous) < 2.0 && i + 1 < samples.size()) + continue; + auto const &next{samples[i].position}; + projection.line(drawlist, previous, next, IM_COL32(255, 150, 40, 200), 2.5f); + glm::dvec3 const designed{next.x, profile::elevation(state.line, samples[i].chainage), next.z}; + if (std::abs(designed.y - next.y) > 0.01 || std::abs(designedprevious.y - previous.y) > 0.01) + projection.line(drawlist, designedprevious, designed, kGradeLine, 2.0f); + previous = next; + designedprevious = designed; + } + auto const &points{state.line.points}; + for (int i = 0; i < static_cast(points.size()); ++i) + { + auto position{editor_track::sampled_position(samples, points[i].chainage)}; + position.y = profile::elevation(state.line, points[i].chainage); + ImVec2 screen; + if (false == projection.project(position, screen)) + continue; + drawlist->AddCircleFilled(screen, i == state.selected ? 7.0f : 5.0f, overlay_color::marked); + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 16.0f), overlay_color::marked, format("%d", i + 1).c_str()); + } + if (state.hover >= 0.0 && state.hover <= state.route.length) + { + ImVec2 screen; + if (projection.project(editor_track::sampled_position(samples, state.hover), screen)) + drawlist->AddCircle(screen, 12.0f, IM_COL32(255, 255, 255, 230), 16, 2.5f); + } +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index db67af75..b3b2c8e2 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -11,6 +11,9 @@ http: #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" #include "utilities/Globals.h" #include "rendering/renderer.h" @@ -31,66 +34,17 @@ http: namespace { -class screen_projection +using geometry::bezier; +using geometry::cross; +using geometry::grade_of; +using geometry::plan_distance; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; + +std::string describe(editor_track::straight const &Line) { - public: - screen_projection() - { - ImGuiIO const &io = ImGui::GetIO(); - m_size = io.DisplaySize; - m_camera = GfxRenderer->Camera_Position(); - float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; - m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); - } - - glm::vec4 clip(glm::dvec3 const &Point) const - { - return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); - } - ImVec2 screen(glm::vec4 const &Clip) const - { - return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); - } - bool project(glm::dvec3 const &Point, ImVec2 &Screen) const - { - auto const c = clip(Point); - if (c.w < kNear) - return false; - Screen = screen(c); - return true; - } - void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const - { - glm::vec4 a = clip(A); - glm::vec4 b = clip(B); - if (a.w < kNear && b.w < kNear) - return; - if (a.w < kNear) - a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); - else if (b.w < kNear) - b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); - Drawlist->AddLine(screen(a), screen(b), Color, Thickness); - } - - private: - static constexpr float kNear{0.1f}; - ImVec2 m_size; - glm::dvec3 m_camera; - glm::mat4 m_viewprojection; -}; - -glm::dvec3 path_point(segment_data const &Path, double const T) -{ - auto const &p0 = Path.points[segment_data::point::start]; - auto const &p3 = Path.points[segment_data::point::end]; - auto const &cp1 = Path.points[segment_data::point::control1]; - auto const &cp2 = Path.points[segment_data::point::control2]; - if (cp1 == glm::dvec3{} && cp2 == glm::dvec3{}) - return glm::mix(p0, p3, T); - auto const p1 = p0 + cp1; - auto const p2 = p3 + cp2; - double const u = 1.0 - T; - return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + return format("L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); } char const *point_label(editor_track::point_kind const Kind) @@ -104,15 +58,16 @@ char const *point_label(editor_track::point_kind const Kind) } } -bool is_end(editor_track::point_kind const Kind) -{ - return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end; -} - editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, editor_track::point_kind::control1, editor_track::point_kind::control2, editor_track::point_kind::end}; float const kHandleRadius{10.0f}; +void item_tooltip(char const *Text) +{ + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", Text); +} + } TTrack *editor_mode::selected_track() const @@ -133,7 +88,10 @@ void editor_mode::select_track(scene::basic_node *Node) return; if (track != m_node) + { m_track_point = {}; + m_track_mode_notice.clear(); + } m_node = track; ui()->set_node(m_node); if (ui()->mode() != nodebank_panel::TRACK) @@ -145,14 +103,16 @@ void editor_mode::select_track(scene::basic_node *Node) } else if (editor_track::is_straight(*track, m_straights.tolerance)) { - m_track_tab_request = 0; m_track_tab = track_tab::straights; } else if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end() || route_from_curve(*track)) { - m_track_tab_request = 1; m_track_tab = track_tab::route; } + else + { + m_track_tab = track_tab::path; + } } bool editor_mode::pick_track_handle() @@ -162,7 +122,6 @@ bool editor_mode::pick_track_handle() return false; screen_projection const projection; - ImVec2 const mouse = ImGui::GetIO().MousePos; float best = kHandleRadius * kHandleRadius; editor_track::point_ref hit; for (int i = 0; i < static_cast(track->m_paths.size()); ++i) @@ -170,12 +129,7 @@ bool editor_mode::pick_track_handle() for (auto const kind : kPointKinds) { editor_track::point_ref const point{i, kind}; - ImVec2 screen; - if (false == projection.project(editor_track::point_position(*track, point), screen)) - continue; - float const dx = screen.x - mouse.x; - float const dy = screen.y - mouse.y; - float const distance = (dx * dx + dy * dy) * (is_end(kind) ? 0.5f : 1.0f); + float const distance = projection.mouse_distance2(editor_track::point_position(*track, point)) * (editor_track::is_end(kind) ? 0.5f : 1.0f); if (distance < best) { best = distance; @@ -203,10 +157,10 @@ void editor_mode::draw_track_overlay() const int constexpr samples{32}; for (auto const &path : track->m_paths) { - auto previous = path_point(path, 0.0); + auto previous = bezier{path}.point(0.0); for (int i = 1; i <= samples; ++i) { - auto const next = path_point(path, static_cast(i) / samples); + auto const next = bezier{path}.point(static_cast(i) / samples); projection.line(drawlist, previous, next, coursecolor, 2.5f); previous = next; } @@ -229,7 +183,7 @@ void editor_mode::draw_track_overlay() const if (false == projection.project(editor_track::point_position(*track, point), screen)) continue; bool const selected = (point == m_track_point); - if (is_end(kind)) + if (editor_track::is_end(kind)) { ImU32 const color = editor_track::is_connected(*track, point) ? IM_COL32(60, 220, 60, 255) : IM_COL32(240, 60, 60, 255); drawlist->AddCircleFilled(screen, 6.0f, color); @@ -262,28 +216,18 @@ void editor_mode::render_track_gizmo() } bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size()); - ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(Global.EditorOrtho); - ImGuiIO const &io = ImGui::GetIO(); - ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); - - glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); - glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); - float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - glm::mat4 const projection = editor_mode::projection_matrix(aspect); + gizmo_frame const frame; ImGuizmo::OPERATION const operation = (false == pointmode && m_gizmo_op == gizmo_operation::rotate) ? ImGuizmo::ROTATE_Y : ImGuizmo::TRANSLATE; if (false == m_track_gizmo_using) { auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track); - m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - camerapos)); + m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); } - glm::vec3 snapvalue(operation == ImGuizmo::ROTATE_Y ? 5.0f : m_gizmo_snap); - float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; glm::mat4 delta(1.0f); - ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), operation, ImGuizmo::WORLD, glm::value_ptr(m_track_gizmo), glm::value_ptr(delta), snap); + frame.manipulate(operation, m_track_gizmo, operation == ImGuizmo::ROTATE_Y ? 5.0f : m_gizmo_snap, &delta); if (ImGuizmo::IsUsing()) { @@ -294,7 +238,7 @@ void editor_mode::render_track_gizmo() m_track_drag = {track}; m_track_drag_points.clear(); m_track_snap = {}; - if (pointmode && is_end(m_track_point.kind) && m_track_drag_connected) + if (pointmode && editor_track::is_end(m_track_point.kind) && m_track_drag_connected) { for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) { @@ -323,12 +267,12 @@ void editor_mode::render_track_gizmo() } else if (pointmode) { - glm::dvec3 const position = camerapos + glm::dvec3(m_track_gizmo[3]); + glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); editor_track::move_point(*track, m_track_point, position); for (auto const &connection : m_track_drag_points) editor_track::move_point(*connection.first, connection.second, position); m_track_dirty = true; - if (is_end(m_track_point.kind) && m_track_drag_points.empty()) + if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) { std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); m_track_snap = editor_track::find_snap_target(*track, position, m_track_snap_radius, exclude); @@ -372,15 +316,19 @@ void editor_mode::commit_track_drag(bool const Force) m_track_last_commit = now; } -void editor_mode::push_track_snapshot(std::vector> States, std::vector Created) +void editor_mode::trim_history() { - if (States.empty() && Created.empty()) - return; - if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); +} - auto *track = States.empty() ? Created.front() : States.front().first; +void editor_mode::push_track_snapshot(std::vector> States, std::vector Created, std::vector Removed) +{ + if (States.empty() && Created.empty() && Removed.empty()) + return; + + trim_history(); + auto *track = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front(); EditorSnapshot snap; snap.action = EditorSnapshot::Action::TrackEdit; snap.node_name = track->name(); @@ -389,6 +337,8 @@ void editor_mode::push_track_snapshot(std::vectoruuid; snap.tracks = std::move(States); snap.created = std::move(Created); + infra_attach(snap); + snap.removed = std::move(Removed); m_history.push_back(std::move(snap)); g_redo.clear(); } @@ -401,6 +351,7 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve std::vector tracks; for (auto const &entry : Snapshot.tracks) tracks.emplace_back(entry.first); + m_infra_suspended = true; EditorSnapshot current{Snapshot}; current.tracks.clear(); auto involved{tracks}; @@ -408,6 +359,10 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve involved.insert(involved.end(), Snapshot.created.begin(), Snapshot.created.end()); for (auto *track : involved) current.tracks.emplace_back(track, editor_track::capture(*track)); + current.infra.clear(); + for (auto const &entry : Snapshot.infra) + if (auto const *binding{infra_find(entry.saved)}) + current.infra.push_back(infra_state_of(*binding)); Opposite.push_back(std::move(current)); if (Undo) @@ -430,10 +385,24 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve for (auto const &entry : Snapshot.tracks) editor_track::apply(*entry.first, entry.second); editor_track::commit(tracks); + for (auto const &entry : Snapshot.infra) + infra_restore_state(entry); + m_infra_buffer.clear(); + m_infra_suspended = false; if (false == m_route.chain.tracks.empty()) { std::string error; - editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error); + if (m_route.from->m_editorremoved || m_route.to->m_editorremoved || false == editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error)) + { + m_route.chain = {}; + m_route.result = {}; + m_route.vertex = m_route.grip = -1; + } + else + { + route_bind_ends(); + route_update(); + } } m_track_snap = {}; @@ -442,6 +411,7 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve m_node = tracks.empty() || tracks.front()->m_editorremoved ? nullptr : tracks.front(); ui()->set_node(m_node); straight_refresh(); + profile_after_undo(); } void editor_mode::render_track_ui() @@ -449,6 +419,16 @@ void editor_mode::render_track_ui() ImGui::TextDisabled("LMB on a path opens its editor: straight, curve or switch"); if (ImGui::Button("Open the editor window")) m_track_window_open = true; + ImGui::SameLine(); + if (ImGui::Button("Vertical profile (grade line)")) + { + if (auto *track{selected_track()}; track != nullptr && m_profile.route.spans.empty()) + profile_open_run(*track); + m_profile.open = true; + } + ImGui::SameLine(); + if (ImGui::Button("Infrastructure")) + m_infra.open = true; if (ImGui::CollapsingHeader("Straights in the scenery")) render_straights_ui(); render_switch_ui(); @@ -472,6 +452,7 @@ void editor_mode::render_track_window() ImGui::End(); return; } + render_track_modes(*selected_track()); switch (m_track_tab) { case track_tab::straights: @@ -491,15 +472,67 @@ void editor_mode::render_track_window() render_path_ui(); break; } - if (m_track_tab != track_tab::path) + ImGui::Separator(); + if (ImGui::SmallButton("Vertical profile along this line") && selected_track() != nullptr) + profile_open_run(*selected_track()); + ImGui::SameLine(); + if (ImGui::SmallButton("Infrastructure along this line")) { - ImGui::Separator(); - if (ImGui::SmallButton("Edit points and parameters of the single path")) - m_track_tab = track_tab::path; + m_infra.open = true; + m_infra.scope = 1; + infra_recognize(); } ImGui::End(); } +void editor_mode::render_track_modes(TTrack &Track) +{ + bool const isswitch{Track.eType == tt_Switch}; + bool const straight{false == isswitch && editor_track::is_straight(Track, m_straights.tolerance)}; + bool const curve{Track.eType == tt_Normal && false == straight && editor_track::is_supported(Track)}; + struct mode + { + char const *label; + track_tab tab; + bool available; + char const *tooltip; + char const *unavailable; + }; + mode const modes[] = { + {"Path", track_tab::path, true, "Points, control vectors and parameters of the selected path", ""}, + {"Straight", track_tab::straights, straight, "The whole straight through the selected path: drag its ends or the middle, break it, shift it", "The selected path isn't straight"}, + {"Curve", track_tab::route, curve, "The curve between the adjoining straights: radius, transitions, cant", "The selected path isn't a curve"}, + {"Switch", track_tab::turnout, isswitch, "Geometry of the switch from a template, side of the diverging track", "The selected path isn't a switch"}, + }; + for (auto const &entry : modes) + { + if (&entry != &modes[0]) + ImGui::SameLine(); + bool const active{m_track_tab == entry.tab}; + if (active) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (false == entry.available) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.4f); + if (ImGui::Button(entry.label, ImVec2(80.0f, 0.0f)) && entry.available && false == active) + { + m_track_mode_notice.clear(); + if (entry.tab != track_tab::route || std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), &Track) != m_route.chain.tracks.end() || route_from_curve(Track)) + m_track_tab = entry.tab; + else + m_track_mode_notice = "No straights found at both ends of the curve, edit it as a single path"; + } + if (false == entry.available) + ImGui::PopStyleVar(); + if (active) + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", entry.available ? entry.tooltip : entry.unavailable); + } + if (false == m_track_mode_notice.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", m_track_mode_notice.c_str()); + ImGui::Separator(); +} + void editor_mode::render_turnout_ui() { auto *track{selected_track()}; @@ -508,9 +541,9 @@ void editor_mode::render_turnout_ui() auto const &main{track->m_paths[0]}; auto const &diverging{track->m_paths[1]}; auto const origin{main.points[segment_data::point::start]}; - glm::dvec2 const axis{glm::normalize(glm::dvec2{main.points[segment_data::point::end].x - origin.x, main.points[segment_data::point::end].z - origin.z})}; + glm::dvec2 const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; glm::dvec2 const normal{-axis.y, axis.x}; - auto const length{glm::dot(glm::dvec2{main.points[segment_data::point::end].x - origin.x, main.points[segment_data::point::end].z - origin.z}, axis)}; + auto const length{glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis)}; glm::dvec2 const end{diverging.points[segment_data::point::end].x - origin.x, diverging.points[segment_data::point::end].z - origin.z}; auto const offset{glm::dot(end, normal)}; auto const &control{diverging.points[segment_data::point::control2]}; @@ -520,7 +553,6 @@ void editor_mode::render_turnout_ui() ImGui::Text("Length %.3f m, diverging %s, end offset %.3f m", length, offset > 0.0 ? "left" : "right", std::abs(offset)); if (angle > 1e-6) ImGui::Text("Angle 1:%.2f (%.4f deg), R in the entry %.0f m", 1.0 / std::tan(angle), glm::degrees(angle), diverging.radius); - ImGui::TextDisabled("Gizmo moves the whole switch (W: turn), drag from a red ring at a free end to add a path"); auto &tool{m_switch}; if (false == tool.collected) @@ -556,6 +588,7 @@ void editor_mode::render_turnout_ui() int const side{offset > 0.0 ? 1 : -1}; if (ImGui::Button("Replace the geometry with the template")) replace(tool.templates[m_turnout_template], side); + item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template"); ImGui::SameLine(); if (ImGui::Button("Flip the side")) { @@ -566,7 +599,7 @@ void editor_mode::render_turnout_ui() { auto &local{own.local[i]}; local = track->m_paths[i]; - auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(glm::dvec2{Vector.x, Vector.z}, normal) * side, Vector.y, glm::dot(glm::dvec2{Vector.x, Vector.z}, axis)}; }; + auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), normal) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; local.points[segment_data::point::start] = tolocal(track->m_paths[i].points[segment_data::point::start] - origin); local.points[segment_data::point::end] = tolocal(track->m_paths[i].points[segment_data::point::end] - origin); local.points[segment_data::point::control1] = tolocal(track->m_paths[i].points[segment_data::point::control1]); @@ -576,6 +609,7 @@ void editor_mode::render_turnout_ui() } replace(own, -side); } + item_tooltip("Mirrors the switch: the diverging track goes to the other side"); ImGui::Separator(); render_path_parameters(*track); } @@ -613,17 +647,22 @@ void editor_mode::render_path_parameters(TTrack &Track) double velocity = editor_track::velocity(*track); if (ImGui::InputDouble("Velocity (km/h, -1: none)", &velocity, 0.0, 0.0, "%.1f")) editor_track::velocity(*track, velocity); + item_tooltip("Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own"); finish_edit(rebuild::none); if (ImGui::InputFloat("Friction", &track->fFriction, 0.0f, 0.0f, "%.3f")) track->fFriction = std::max(0.0f, track->fFriction); + item_tooltip("Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip"); finish_edit(rebuild::none); ImGui::InputFloat("Sound distance", &track->fSoundDistance, 0.0f, 0.0f, "%.1f"); + item_tooltip("Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default"); finish_edit(rebuild::none); ImGui::InputInt("Quality", &track->iQualityFlag, 0); + item_tooltip("Quality of the track, 20 is the nominal one; lower values give a rougher ride"); finish_edit(rebuild::none); int damage = track->iDamageFlag; if (ImGui::InputInt("Damage flags", &damage, 0)) editor_track::damage(*track, damage); + item_tooltip("0: track in order; any value adds jolts, 128: the vehicles derail on the path"); finish_edit(rebuild::none); char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; @@ -633,9 +672,11 @@ void editor_mode::render_path_parameters(TTrack &Track) track->eEnvironment = static_cast(environment); apply_now(rebuild::none); } + item_tooltip("Surroundings of the path, they affect the sound of the passing vehicles"); if (ImGui::InputFloat("Width", &track->fTrackWidth, 0.0f, 0.0f, "%.3f")) track->fTrackWidth = std::max(0.01f, track->fTrackWidth); + item_tooltip("Gauge of the track (1.435 m for the standard gauge), width of the surface for the roads"); finish_edit(rebuild::parameters); ImGui::PopItemWidth(); @@ -649,6 +690,8 @@ void editor_mode::render_path_parameters(TTrack &Track) { for (int i = 0; i < 3; ++i) { + if (m_track_material_edited[i] != nullptr) + continue; auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{}; std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1); m_track_materials[i].back() = '\0'; @@ -659,26 +702,37 @@ void editor_mode::render_path_parameters(TTrack &Track) if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch)) continue; ImGui::PushItemWidth(-160.0f); - if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size(), ImGuiInputTextFlags_EnterReturnsTrue)) + if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size())) + m_track_material_edited[i] = track; + if (ImGui::IsItemDeactivated() && m_track_material_edited[i] != nullptr) { - *materials[i] = editor_track::fetch_material(m_track_materials[i].data()); - apply_now(rebuild::parameters); + auto const *edited{m_track_material_edited[i]}; + m_track_material_edited[i] = nullptr; + if (edited == track && m_track_materials[i].data() != editor_track::material_name(*materials[i])) + { + *materials[i] = editor_track::fetch_material(m_track_materials[i].data()); + apply_now(rebuild::parameters); + } } ImGui::PopItemWidth(); } - ImGui::TextDisabled("Enter applies a texture name, \"none\" removes the texture"); + ImGui::TextDisabled("The texture changes on Enter or leaving the field, \"none\" removes it"); ImGui::PushItemWidth(120.0f); if (ImGui::InputFloat("Texture length", &track->fTexLength, 0.0f, 0.0f, "%.2f")) track->fTexLength = std::max(0.01f, track->fTexLength); + item_tooltip("Length in metres after which the texture of the rails and the trackbed repeats"); finish_edit(rebuild::parameters); float height = editor_track::texture_height(*track); if (ImGui::InputFloat("Trackbed height", &height, 0.0f, 0.0f, "%.3f")) editor_track::texture_height(*track, height); + item_tooltip("Height of the trackbed edge below the rail top, in metres"); finish_edit(rebuild::parameters); ImGui::InputFloat("Trackbed width", &track->fTexWidth, 0.0f, 0.0f, "%.3f"); + item_tooltip("Width of the trackbed outside of the rail, in metres"); finish_edit(rebuild::parameters); ImGui::InputFloat("Trackbed slope", &track->fTexSlope, 0.0f, 0.0f, "%.3f"); + item_tooltip("Horizontal reach of the slope at the outer edge of the trackbed, in metres"); finish_edit(rebuild::parameters); ImGui::PopItemWidth(); } @@ -688,7 +742,6 @@ void editor_mode::render_path_parameters(TTrack &Track) void editor_mode::render_path_ui() { - ImGui::TextDisabled("LMB: select path or point handle Esc: release point\nGizmo moves the selected point, otherwise the whole path (W: turn)"); auto *track = selected_track(); if (track == nullptr) { @@ -740,11 +793,14 @@ void editor_mode::render_path_ui() { ImGui::PushItemWidth(120.0f); ImGui::InputFloat("Snap radius (m)", &m_track_snap_radius, 0.0f, 0.0f, "%.2f"); + item_tooltip("A dragged free end within this distance of another free end joins it on release"); m_track_snap_radius = std::max(0.0f, m_track_snap_radius); ImGui::PopItemWidth(); ImGui::Checkbox("Align with snapped path", &m_track_align_tangent); + item_tooltip("On joining, the end also takes over the direction of the other path, so the joint has no kink"); ImGui::SameLine(); ImGui::Checkbox("Drag connected ends", &m_track_drag_connected); + item_tooltip("Dragging a joined end moves the ends of the neighbouring paths with it"); for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { @@ -757,7 +813,7 @@ void editor_mode::render_path_ui() if (kind != editor_track::point_kind::start) ImGui::SameLine(); std::string label = point_label(kind); - if (is_end(kind)) + if (editor_track::is_end(kind)) label += editor_track::is_connected(*track, point) ? " (+)" : " (-)"; if (ImGui::Selectable(label.c_str(), m_track_point == point, 0, ImVec2(90.0f, 0.0f))) m_track_point = (m_track_point == point ? editor_track::point_ref{} : point); @@ -775,12 +831,16 @@ void editor_mode::render_path_ui() finish_edit(rebuild::geometry); } ImGui::InputScalarN("Control 1 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control1]), 3, nullptr, nullptr, "%.3f"); + item_tooltip("Control vector of the start, relative to the start point; zero for a straight"); finish_edit(rebuild::geometry); ImGui::InputScalarN("Control 2 (relative)", ImGuiDataType_Double, glm::value_ptr(path.points[segment_data::point::control2]), 3, nullptr, nullptr, "%.3f"); + item_tooltip("Control vector of the end, relative to the end point; zero for a straight"); finish_edit(rebuild::geometry); ImGui::InputFloat2("Roll (deg)", path.rolls.data(), "%.2f"); + item_tooltip("Cant at the start and at the end as the roll angle of the track"); finish_edit(rebuild::geometry); ImGui::InputFloat("Radius", &path.radius, 0.0f, 0.0f, "%.1f"); + item_tooltip("Radius stored with the path in the scenery; the shape itself comes from the control vectors"); finish_edit(rebuild::geometry); ImGui::PopItemWidth(); if (ImGui::Button("Straighten")) @@ -791,6 +851,7 @@ void editor_mode::render_path_ui() path.radius = 0.0f; editor_track::commit({track}); } + item_tooltip("Removes the control vectors and the radius, the path becomes a straight between its ends"); } else { @@ -815,7 +876,22 @@ void editor_mode::route_recommend(alignment::vertex &Vertex) const Vertex.cant = recommended.cant; Vertex.transition_in = recommended.transition; Vertex.transition_out = recommended.transition; - Vertex.vertical_radius = 0.0; +} + +void editor_mode::route_bind_ends() +{ + auto const &chain{m_route.chain}; + auto &design{m_route.design}; + design.start = chain.start; + design.end = chain.end; + design.start_direction = glm::normalize(plan_of(chain.start_direction)); + design.end_direction = glm::normalize(plan_of(chain.end_direction)); + design.start_grade = grade_of(chain.start_direction); + design.end_grade = grade_of(chain.end_direction); + design.start_radius = chain.start_radius; + design.end_radius = chain.end_radius; + design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); + design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); } void editor_mode::route_reset() @@ -837,18 +913,10 @@ void editor_mode::route_reset() design.norms = route.design.norms; design.shape = route.design.shape; design.transition_pieces = route.design.transition_pieces; - design.start = chain.start; - design.end = chain.end; - auto const plan = [](glm::dvec3 const &Direction) { return glm::normalize(glm::dvec2{Direction.x, Direction.z}); }; - auto const slope = [](glm::dvec3 const &Direction) { return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); }; - design.start_direction = plan(chain.start_direction); - design.end_direction = plan(chain.end_direction); - design.start_grade = slope(chain.start_direction); - design.end_grade = slope(chain.end_direction); - design.start_radius = chain.start_radius; - design.end_radius = chain.end_radius; design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; route.design = design; + route_bind_ends(); + design = route.design; glm::dvec2 intersection; if (false == alignment::collinear(design)) @@ -862,7 +930,7 @@ void editor_mode::route_reset() } else { - auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; + auto const distance{plan_distance(design.start, design.end)}; vertex.offset = distance / 3.0; route.design.vertices.push_back(vertex); route.design.vertices.push_back(vertex); @@ -905,7 +973,7 @@ bool editor_mode::route_from_curve(TTrack &Track) } else { - auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; + auto const distance{plan_distance(design.start, design.end)}; vertex.offset = distance / 3.0; design.vertices = {vertex, vertex}; } @@ -938,16 +1006,60 @@ void editor_mode::route_apply() } } - auto const pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; std::vector> states; + std::vector created; + auto *from{route.from}; + auto *to{route.to}; + auto const startextension{route.result.start_extension}; + auto const endextension{route.result.end_extension}; + if (startextension > 0.0 || endextension > 0.0) + { + auto const before{chain}; + if (startextension > 0.0) + from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created); + if (endextension > 0.0) + to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); + std::string error; + if (false == editor_track::find_chain(from, to, chain, error) || plan_distance(chain.start, route.result.start) > 5.0 || plan_distance(chain.end, route.result.end) > 5.0) + { + for (auto *track : created) + editor_track::retire(*track); + for (auto entry = states.rbegin(); entry != states.rend(); ++entry) + { + editor_track::apply(*entry->first, entry->second); + editor_track::commit({entry->first}); + } + route.error = error.empty() ? "The adjoining straights can't be cut at the ends of the curve" : error; + return; + } + } for (auto *track : chain.tracks) - states.emplace_back(track, editor_track::capture(*track)); - auto created{editor_track::relay(chain, pieces)}; - auto const added{created.size()}; + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + editor_track::keep_heights(chain, pieces); + auto relaid{editor_track::relay(chain, pieces)}; + auto const added{relaid.size()}; + created.insert(created.end(), relaid.begin(), relaid.end()); push_track_snapshot(std::move(states), std::move(created)); + route.from = from; + route.to = to; editor_track::find_chain(route.from, route.to, route.chain, route.error); + if (startextension > 0.0 || endextension > 0.0) + { + route_bind_ends(); + auto &vertices{route.design.vertices}; + if (vertices.size() >= 2) + { + vertices.front().offset += startextension; + vertices.back().offset += endextension; + } + route_update(); + } route.status = "Re-laid " + std::to_string(chain.tracks.size()) + " paths" + (added > 0 ? ", added " + std::to_string(added) : std::string{}); + if (startextension > 0.0 || endextension > 0.0) + route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); WriteLog("Editor: route design - " + route.status, logtype::generic); } @@ -973,13 +1085,7 @@ glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const else position = design.vertices[Vertex].position; - double height; - if (Vertex < static_cast(result.vertex_elevations.size())) - height = result.vertex_elevations[Vertex]; - else if (false == design.vertices[Vertex].auto_elevation) - height = design.vertices[Vertex].elevation; - else - height = design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0); + auto const height{Vertex < static_cast(result.vertex_elevations.size()) ? result.vertex_elevations[Vertex] : design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0)}; return {position.x, height, position.y}; } @@ -988,33 +1094,22 @@ bool editor_mode::pick_route_vertex() if (false == route_active()) return false; screen_projection const projection; - ImVec2 const mouse = ImGui::GetIO().MousePos; float best = kHandleRadius * kHandleRadius; int hit{-1}; for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) { - ImVec2 screen; - if (false == projection.project(route_vertex_position(i), screen)) - continue; - float const dx = screen.x - mouse.x; - float const dy = screen.y - mouse.y; - if (dx * dx + dy * dy < best) + if (auto const distance{projection.mouse_distance2(route_vertex_position(i))}; distance < best) { - best = dx * dx + dy * dy; + best = distance; hit = i; } } int griphit{-1}; for (int i = 0; i < static_cast(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i) { - ImVec2 screen; - if (false == projection.project(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position, screen)) - continue; - float const dx = screen.x - mouse.x; - float const dy = screen.y - mouse.y; - if (dx * dx + dy * dy < best) + if (auto const distance{projection.mouse_distance2(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position)}; distance < best) { - best = dx * dx + dy * dy; + best = distance; griphit = i; hit = -1; } @@ -1033,11 +1128,13 @@ void editor_mode::draw_route_overlay() const auto const &result{route.result}; screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + auto const &start{result.valid ? result.start : design.start}; + auto const &end{result.valid ? result.end : design.end}; - std::vector polygon{design.start}; + std::vector polygon{start}; for (int i = 0; i < static_cast(design.vertices.size()); ++i) polygon.push_back(route_vertex_position(i)); - polygon.push_back(design.end); + polygon.push_back(end); for (std::size_t i = 0; i + 1 < polygon.size(); ++i) projection.line(drawlist, polygon[i], polygon[i + 1], IM_COL32(255, 210, 60, 150), 1.5f); @@ -1045,7 +1142,7 @@ void editor_mode::draw_route_overlay() const { for (auto const &e : result.elements) { - ImU32 const color = e.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : e.kind == alignment::element_kind::arc ? IM_COL32(60, 230, 90, 255) : IM_COL32(255, 140, 30, 255); + ImU32 const color = e.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : e.kind == alignment::element_kind::arc ? overlay_color::grip : IM_COL32(255, 140, 30, 255); int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); auto previous{alignment::evaluate(result, e.chainage).position}; for (int j = 1; j <= steps; ++j) @@ -1056,13 +1153,13 @@ void editor_mode::draw_route_overlay() const } ImVec2 screen; if (projection.project(alignment::evaluate(result, e.chainage).position, screen)) - drawlist->AddCircleFilled(screen, 3.5f, IM_COL32(255, 255, 255, 255)); + drawlist->AddCircleFilled(screen, 3.5f, overlay_color::highlight); } } for (auto const atend : {false, true}) { - auto const &point{atend ? design.end : design.start}; + auto const &point{atend ? end : start}; auto const direction{atend ? design.end_direction : design.start_direction}; ImVec2 screen; if (projection.project(point, screen)) @@ -1075,25 +1172,19 @@ void editor_mode::draw_route_overlay() const ImVec2 screen; if (false == projection.project(route_vertex_position(i), screen)) continue; - drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(255, 210, 60, 255)); + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::marked); if (i == route.vertex) - drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); if (i < static_cast(result.vertex_chainages.size()) && result.length > 0.0 && projection.project(alignment::evaluate(result, result.vertex_chainages[i]).position, screen)) { - drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(60, 230, 90, 255), 4); + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::grip, 4); if (i == route.grip) - drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); - char radius[40]; + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; - if (report != nullptr && report->radius2 > 0.0) - std::snprintf(radius, sizeof(radius), "R %.0f / %.0f", report->radius, report->radius2); - else - std::snprintf(radius, sizeof(radius), "R %.0f", report != nullptr ? report->radius : design.vertices[i].radius); - drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), IM_COL32(60, 230, 90, 255), radius); + auto const radius{report != nullptr && report->radius2 > 0.0 ? format("R %.0f / %.0f", report->radius, report->radius2) : format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)}; + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str()); } - char label[8]; - std::snprintf(label, sizeof(label), "W%d", i + 1); - drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), IM_COL32(255, 210, 60, 255), label); + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); } } @@ -1108,21 +1199,12 @@ void editor_mode::render_route_gizmo() return; } - ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(Global.EditorOrtho); - ImGuiIO const &io = ImGui::GetIO(); - ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); - glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); - glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); - float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - glm::mat4 const projection = editor_mode::projection_matrix(aspect); + gizmo_frame const frame; auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; if (false == m_route_gizmo_using) - m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - camerapos)); - glm::vec3 snapvalue(m_gizmo_snap); - float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; - ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(m_route_gizmo), nullptr, snap); + m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, m_route_gizmo, m_gizmo_snap); if (false == ImGuizmo::IsUsing()) { @@ -1134,12 +1216,12 @@ void editor_mode::render_route_gizmo() return; } m_route_gizmo_using = true; - glm::dvec3 const moved{camerapos + glm::dvec3(m_route_gizmo[3])}; + glm::dvec3 const moved{frame.camera + glm::dvec3(m_route_gizmo[3])}; if (grip) { auto &vertex{route.design.vertices[route.grip]}; auto const corner{route_vertex_position(route.grip)}; - auto const distance{glm::distance(glm::dvec2{moved.x, moved.z}, glm::dvec2{corner.x, corner.z})}; + auto const distance{plan_distance(moved, corner)}; for (auto const &curve : route.result.curves) { if (curve.vertex != route.grip || vertex.reverse_turn) @@ -1156,28 +1238,45 @@ void editor_mode::render_route_gizmo() if (count >= 2) { if (route.vertex == 0) - vertex.offset = std::max(0.1, glm::dot(point - glm::dvec2{route.design.start.x, route.design.start.z}, glm::normalize(route.design.start_direction))); + vertex.offset = std::max(0.1, glm::dot(point - plan_of(route.design.start), glm::normalize(route.design.start_direction))); else if (route.vertex == count - 1) - vertex.offset = std::max(0.1, glm::dot(glm::dvec2{route.design.end.x, route.design.end.z} - point, glm::normalize(route.design.end_direction))); + vertex.offset = std::max(0.1, glm::dot(plan_of(route.design.end) - point, glm::normalize(route.design.end_direction))); else vertex.position = point; } - if (std::abs(moved.y - position.y) > 1e-3) - { - vertex.auto_elevation = false; - vertex.elevation = moved.y; - } route_update(); } void editor_mode::render_route_ui() { - auto &route{m_route}; - auto &design{route.design}; ImGui::TextDisabled("LMB on a curve: loads the whole curve between the adjoining straights.\n" "The ends of the curve and their directions stay fixed.\n" "LMB on a vertex (W1, W2...): moves it with the gizmo Esc: release the vertex"); + render_route_ends(); + auto &route{m_route}; + if (route.chain.tracks.empty()) + return; + + ImGui::Text("Fragment: %zu paths, %.2f m", route.chain.tracks.size(), route.chain.length); + ImGui::SameLine(); + if (ImGui::SmallButton("Reset design")) + { + route_reset(); + return; + } + auto changed{render_route_parameters()}; + changed |= render_route_vertices(); + if (changed) + route_update(); + + ImGui::Separator(); + render_route_result(); +} + +void editor_mode::render_route_ends() +{ + auto &route{m_route}; auto *track = selected_track(); auto const name = [](TTrack const *Track) { return Track == nullptr ? std::string{"-"} : (Track->name().empty() ? std::string{"(noname)"} : Track->name()); }; ImGui::Text("First path: %s", name(route.from).c_str()); @@ -1200,20 +1299,15 @@ void editor_mode::render_route_ui() } if (false == route.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); - if (route.chain.tracks.empty()) - return; - - ImGui::Text("Fragment: %zu paths, %.2f m", route.chain.tracks.size(), route.chain.length); - ImGui::SameLine(); - if (ImGui::SmallButton("Reset design")) - { - route_reset(); - return; - } +} +bool editor_mode::render_route_parameters() +{ + auto &design{m_route.design}; bool changed{false}; ImGui::PushItemWidth(120.0f); changed |= ImGui::InputDouble("Design speed (km/h)", &design.speed, 0.0, 0.0, "%.0f"); + item_tooltip("Speed the cant, the transitions and the recommended radius are designed for"); design.speed = std::max(1.0, design.speed); int shape = static_cast(design.shape); char const *shapes[] = {"Cubic parabola", "Clothoid"}; @@ -1222,8 +1316,10 @@ void editor_mode::render_route_ui() design.shape = static_cast(shape); changed = true; } + item_tooltip("Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length"); if (ImGui::SliderInt("Paths per transition curve", &design.transition_pieces, 1, 4)) route_update(); + item_tooltip("More paths follow the transition curve more closely in the scenery"); if (ImGui::IsItemDeactivatedAfterEdit()) route_apply(); ImGui::PopItemWidth(); @@ -1238,8 +1334,6 @@ void editor_mode::render_route_ui() changed |= ImGui::InputDouble("Cant change rate (mm/s)", &norms.cant_rate, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Cant ramp 1:(k*V), k", &norms.ramp_factor, 0.0, 0.0, "%.1f"); changed |= ImGui::InputDouble("Unbalanced acc. change rate (m/s3)", &norms.jerk, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble("Vertical curve R >= k*V^2, k", &norms.vertical_factor, 0.0, 0.0, "%.2f"); - changed |= ImGui::InputDouble("Vertical curve R min (m)", &norms.vertical_min, 0.0, 0.0, "%.0f"); changed |= ImGui::InputDouble("Straight between curves (s of travel)", &norms.tangent_min_time, 0.0, 0.0, "%.1f"); ImGui::PopItemWidth(); norms.gauge = std::max(100.0, norms.gauge); @@ -1247,14 +1341,21 @@ void editor_mode::render_route_ui() norms.jerk = std::max(0.01, norms.jerk); ImGui::TreePop(); } + return changed; +} +bool editor_mode::render_route_vertices() +{ + auto &route{m_route}; + auto &design{route.design}; + bool changed{false}; auto &vertices{design.vertices}; if (ImGui::Button("Add vertex")) { alignment::vertex vertex; vertex.radius = vertices.empty() ? 1000.0 : vertices.back().radius; route_recommend(vertex); - auto const distance{glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z})}; + auto const distance{plan_distance(design.start, design.end)}; if (vertices.size() == 1) { vertices.front().offset = distance / 3.0; @@ -1292,84 +1393,97 @@ void editor_mode::render_route_ui() auto const count{static_cast(vertices.size())}; for (int i = 0; i < count; ++i) { - auto &vertex{vertices[i]}; ImGui::PushID(i); - char header[32]; - std::snprintf(header, sizeof(header), "Vertex W%d", i + 1); - if (ImGui::Selectable(header, route.vertex == i)) - route.vertex = (route.vertex == i ? -1 : i); - ImGui::Indent(); - ImGui::PushItemWidth(110.0f); - if (count == 1) - ImGui::TextDisabled("At the intersection of the end tangents"); - else if (i == 0) - changed |= ImGui::InputDouble("Distance from the start (m)", &vertex.offset, 0.0, 0.0, "%.2f"); - else if (i == count - 1) - changed |= ImGui::InputDouble("Distance from the end (m)", &vertex.offset, 0.0, 0.0, "%.2f"); - else - { - ImGui::PushItemWidth(220.0f); - changed |= ImGui::InputScalarN("Position (x, z)", ImGuiDataType_Double, glm::value_ptr(vertex.position), 2, nullptr, nullptr, "%.2f"); - ImGui::PopItemWidth(); - } - changed |= ImGui::InputDouble(vertex.compound ? "Radius R1 (m)" : "Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f"); - changed |= ImGui::Checkbox("Compound curve (two radii)", &vertex.compound); - if (vertex.compound) - { - changed |= ImGui::InputDouble("Radius R2 (m)", &vertex.radius2, 0.0, 0.0, "%.1f"); - changed |= ImGui::InputDouble("Transition R1-R2 (m)", &vertex.transition_middle, 0.0, 0.0, "%.1f"); - float split{static_cast(vertex.split)}; - if (ImGui::SliderFloat("Share of R1", &split, 0.0f, 1.0f, "%.2f")) - { - vertex.split = split; - changed = true; - } - vertex.radius2 = std::max(1.0, vertex.radius2); - vertex.transition_middle = std::max(0.0, vertex.transition_middle); - } - changed |= ImGui::InputDouble("Transition in (m)", &vertex.transition_in, 0.0, 0.0, "%.1f"); - changed |= ImGui::InputDouble("Transition out (m)", &vertex.transition_out, 0.0, 0.0, "%.1f"); - changed |= ImGui::InputDouble("Cant (mm)", &vertex.cant, 0.0, 0.0, "%.0f"); - changed |= ImGui::Checkbox("Turn the longer way (over 180 deg)", &vertex.reverse_turn); - changed |= ImGui::Checkbox("Elevation from the ends", &vertex.auto_elevation); - if (false == vertex.auto_elevation) - changed |= ImGui::InputDouble("Elevation (m)", &vertex.elevation, 0.0, 0.0, "%.3f"); - changed |= ImGui::InputDouble("Vertical curve R (0: min)", &vertex.vertical_radius, 0.0, 0.0, "%.0f"); - ImGui::PopItemWidth(); - vertex.radius = std::max(1.0, vertex.radius); - vertex.transition_in = std::max(0.0, vertex.transition_in); - vertex.transition_out = std::max(0.0, vertex.transition_out); - vertex.cant = std::max(0.0, vertex.cant); - vertex.vertical_radius = std::max(0.0, vertex.vertical_radius); - if (ImGui::SmallButton("Apply recommended for V")) - { - route_recommend(vertex); - changed = true; - } - - auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; - ImGui::TextDisabled("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m", design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); - for (auto const &curve : route.result.curves) - { - if (curve.vertex != i) - continue; - ImGui::Text("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m", glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length); - ImGui::Text("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3", curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out); - } - ImGui::Unindent(); + changed |= render_route_vertex(i); ImGui::PopID(); } - if (changed) - route_update(); + return changed; +} - ImGui::Separator(); +bool editor_mode::render_route_vertex(int const Index) +{ + auto &route{m_route}; + auto &design{route.design}; + auto &vertex{design.vertices[Index]}; + auto const count{static_cast(design.vertices.size())}; + bool changed{false}; + if (ImGui::Selectable(format("Vertex W%d", Index + 1).c_str(), route.vertex == Index)) + route.vertex = (route.vertex == Index ? -1 : Index); + ImGui::Indent(); + ImGui::PushItemWidth(110.0f); + if (count == 1) + ImGui::TextDisabled("At the intersection of the end tangents"); + else if (Index == 0) + changed |= ImGui::InputDouble("Distance from the start (m)", &vertex.offset, 0.0, 0.0, "%.2f"); + else if (Index == count - 1) + changed |= ImGui::InputDouble("Distance from the end (m)", &vertex.offset, 0.0, 0.0, "%.2f"); + else + { + ImGui::PushItemWidth(220.0f); + changed |= ImGui::InputScalarN("Position (x, z)", ImGuiDataType_Double, glm::value_ptr(vertex.position), 2, nullptr, nullptr, "%.2f"); + ImGui::PopItemWidth(); + } + changed |= ImGui::InputDouble(vertex.compound ? "Radius R1 (m)" : "Radius R (m)", &vertex.radius, 0.0, 0.0, "%.1f"); + changed |= ImGui::Checkbox("Compound curve (two radii)", &vertex.compound); + item_tooltip("Curve made of two arcs of different radii, joined with a transition"); + if (vertex.compound) + { + changed |= ImGui::InputDouble("Radius R2 (m)", &vertex.radius2, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Transition R1-R2 (m)", &vertex.transition_middle, 0.0, 0.0, "%.1f"); + float split{static_cast(vertex.split)}; + if (ImGui::SliderFloat("Share of R1", &split, 0.0f, 1.0f, "%.2f")) + { + vertex.split = split; + changed = true; + } + vertex.radius2 = std::max(1.0, vertex.radius2); + vertex.transition_middle = std::max(0.0, vertex.transition_middle); + } + changed |= ImGui::InputDouble("Transition in (m)", &vertex.transition_in, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Transition out (m)", &vertex.transition_out, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble("Cant (mm)", &vertex.cant, 0.0, 0.0, "%.0f"); + item_tooltip("Height of the outer rail above the inner one"); + changed |= ImGui::Checkbox("Turn the longer way (over 180 deg)", &vertex.reverse_turn); + item_tooltip("For the loops: the curve turns by more than a half circle"); + ImGui::PopItemWidth(); + vertex.radius = std::max(1.0, vertex.radius); + vertex.transition_in = std::max(0.0, vertex.transition_in); + vertex.transition_out = std::max(0.0, vertex.transition_out); + vertex.cant = std::max(0.0, vertex.cant); + if (ImGui::SmallButton("Apply recommended for V")) + { + route_recommend(vertex); + changed = true; + } + item_tooltip("Cant and transitions recommended for the design speed and the radius"); + + auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; + ImGui::TextDisabled("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m", design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); + for (auto const &curve : route.result.curves) + { + if (curve.vertex != Index) + continue; + ImGui::Text("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m", glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length); + ImGui::Text("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3", curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out); + } + ImGui::Unindent(); + return changed; +} + +void editor_mode::render_route_result() +{ + auto const &route{m_route}; + auto const &design{route.design}; auto const &result{route.result}; if (result.length > 0.0) { auto const pieces{alignment::minimum_pieces(result, design)}; ImGui::Text("Length %.2f m (now %.2f m), %zu elements", result.length, route.chain.length, result.elements.size()); ImGui::TextDisabled("Paths: %zu in the fragment, %zu needed%s", route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? " (copies of the neighbours will be added)" : ""); + if (result.start_extension > 0.0 || result.end_extension > 0.0) + ImGui::Text("Takes over %.2f m of the straight at the start, %.2f m at the end", result.start_extension, result.end_extension); } + ImGui::TextDisabled("Straight available beyond the ends: %.2f / %.2f m", design.start_reserve, design.end_reserve); for (auto const &error : result.errors) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str()); for (auto const &warning : result.warnings) @@ -1396,8 +1510,7 @@ void editor_mode::render_route_ui() { ImGui::SameLine(); ImGui::TextUnformatted(route.status.c_str()); - } -} + }} editor_track::straight const &editor_mode::current_straight() { @@ -1419,9 +1532,9 @@ void editor_mode::draw_straights_overlay() const glm::dvec3 const camera{Global.pCamera.Pos}; auto const nearby = [&](editor_track::straight const &Line) { auto const offset{camera - Line.start}; - auto const along{std::clamp(glm::dot(glm::dvec2{offset.x, offset.z}, Line.direction), 0.0, Line.length)}; + auto const along{std::clamp(glm::dot(plan_of(offset), Line.direction), 0.0, Line.length)}; auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along}; - return glm::distance(glm::dvec2{closest.x, closest.z}, glm::dvec2{camera.x, camera.z}) < 3000.0; + return plan_distance(closest, camera) < 3000.0; }; auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) { if (Line.tracks.empty()) @@ -1438,40 +1551,40 @@ void editor_mode::draw_straights_overlay() const { auto const &line{m_straights.found[i]}; if (nearby(line)) - draw(line, i == m_straights.listed ? IM_COL32(40, 220, 255, 255) : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f); + draw(line, i == m_straights.listed ? overlay_color::selected : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f); } for (auto const &member : m_straights.set) draw(member, IM_COL32(255, 150, 30, 255), 4.0f); - draw(m_straights.current, IM_COL32(40, 220, 255, 255), 4.0f); + draw(m_straights.current, overlay_color::selected, 4.0f); if (m_straights.tool_placed) { auto const &state{m_straights}; auto const &line{state.tool_line}; ImVec2 screen; if (projection.project(state.tool_point, screen)) - drawlist->AddCircleFilled(screen, 7.0f, IM_COL32(255, 60, 60, 255)); + drawlist->AddCircleFilled(screen, 7.0f, overlay_color::invalid); if (projection.project(state.tool_handle, screen)) - drawlist->AddCircleFilled(screen, 7.0f, IM_COL32(255, 210, 60, 255)); + drawlist->AddCircleFilled(screen, 7.0f, overlay_color::marked); if (state.tool == 1) { - auto const direction{glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}}; + auto const direction{plan_of(state.tool_handle - state.tool_point)}; if (glm::length(direction) > 1e-3) { auto const unit{glm::normalize(direction)}; auto const rest{std::max(0.0, line.length - state.tool_at)}; - projection.line(drawlist, state.tool_point, state.tool_point + glm::dvec3{unit.x, line.grade, unit.y} * rest, IM_COL32(255, 210, 60, 255), 3.0f); + projection.line(drawlist, state.tool_point, state.tool_point + glm::dvec3{unit.x, line.grade, unit.y} * rest, overlay_color::marked, 3.0f); } } else { glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto const offset{glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; auto const shift{glm::dot(offset, normal)}; auto const length{glm::dot(offset, line.direction)}; auto const shifted{glm::dvec3{normal.x, 0.0, normal.y} * shift}; auto const along{glm::dvec3{line.direction.x, 0.0, line.direction.y}}; projection.line(drawlist, state.tool_point, state.tool_point + along * length + shifted, IM_COL32(255, 210, 60, 160), 2.0f); - projection.line(drawlist, state.tool_point + along * length + shifted, line.end + shifted, IM_COL32(255, 210, 60, 255), 3.0f); + projection.line(drawlist, state.tool_point + along * length + shifted, line.end + shifted, overlay_color::marked, 3.0f); } } if (m_straights.dragging && m_straights.fitting) @@ -1484,7 +1597,7 @@ void editor_mode::draw_straights_overlay() const for (int i = 1; i <= steps; ++i) { auto const next{alignment::evaluate(result, result.length * i / steps).position}; - projection.line(drawlist, previous, next, result.valid ? IM_COL32(255, 210, 60, 255) : IM_COL32(255, 60, 60, 255), 3.0f); + projection.line(drawlist, previous, next, result.valid ? overlay_color::marked : overlay_color::invalid, 3.0f); previous = next; } } @@ -1496,7 +1609,7 @@ void editor_mode::draw_straights_overlay() const for (auto const *track : member.tracks) { auto const &path{track->m_paths.front()}; - projection.line(drawlist, path.points[segment_data::point::start], path.points[segment_data::point::end], IM_COL32(255, 210, 60, 255), 3.0f); + projection.line(drawlist, path.points[segment_data::point::start], path.points[segment_data::point::end], overlay_color::marked, 3.0f); } } } @@ -1510,11 +1623,11 @@ void editor_mode::draw_straights_overlay() const if (false == projection.project(handles[i], screen)) continue; if (i < 2) - drawlist->AddRectFilled(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), IM_COL32(40, 220, 255, 255)); + drawlist->AddRectFilled(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), overlay_color::selected); else - drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(40, 220, 255, 255), 4); + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::selected, 4); if (i == m_straights.handle) - drawlist->AddCircle(screen, 12.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); + drawlist->AddCircle(screen, 12.0f, overlay_color::highlight, 16, 2.5f); } } auto const &line{m_straights.current}; @@ -1523,9 +1636,8 @@ void editor_mode::draw_straights_overlay() const glm::dvec2 const normal{-line.direction.y, line.direction.x}; for (auto const &other : m_straights.neighbours) { - auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; - auto const across = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, normal); }; - auto const a0{along(other.start)}, a1{along(other.end)}; + auto const across = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - line.start), normal); }; + auto const a0{line.along(other.start)}, a1{line.along(other.end)}; auto const from{std::max(0.0, std::min(a0, a1))}; auto const to{std::min(line.length, std::max(a0, a1))}; if (to <= from) @@ -1540,11 +1652,8 @@ void editor_mode::draw_straights_overlay() const if (projection.project((foot + target) * 0.5, screen)) { auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; - char label[48]; - if (angle > 0.001) - std::snprintf(label, sizeof(label), "%.3f m %.3f deg", std::abs(lateral), angle); - else - std::snprintf(label, sizeof(label), "%.3f m", std::abs(lateral)); + auto const text{angle > 0.001 ? format("%.3f m %.3f deg", std::abs(lateral), angle) : format("%.3f m", std::abs(lateral))}; + auto const *label{text.c_str()}; auto const size{ImGui::CalcTextSize(label)}; drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 3.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 170), 3.0f); drawlist->AddText(screen, IM_COL32(255, 230, 120, 255), label); @@ -1578,8 +1687,8 @@ void editor_mode::find_neighbour_straights() auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}}; if (std::abs(glm::dot(offset, normal)) > 60.0) continue; - auto const a0{glm::dot(glm::dvec2{other.start.x - line.start.x, other.start.z - line.start.z}, line.direction)}; - auto const a1{glm::dot(glm::dvec2{other.end.x - line.start.x, other.end.z - line.start.z}, line.direction)}; + auto const a0{line.along(other.start)}; + auto const a1{line.along(other.end)}; if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length) continue; state.neighbours.push_back(std::move(other)); @@ -1598,7 +1707,7 @@ glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, { if (std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x) > std::sin(glm::radians(0.5))) continue; - auto const distance{glm::dot(glm::dvec2{other.start.x - middle.x, other.start.z - middle.z}, normal)}; + auto const distance{glm::dot(plan_of(other.start - middle), normal)}; for (auto const spacing : m_straights.spacings) { auto const error{std::abs(distance) - spacing}; @@ -1639,9 +1748,13 @@ void editor_mode::render_straights_ui() double angle{glm::degrees(state.tolerance.angle)}; if (ImGui::InputDouble("Angle tolerance (deg)", &angle, 0.0, 0.0, "%.4f")) state.tolerance.angle = glm::radians(std::max(0.0, angle)); + item_tooltip("Largest change of the direction between the paths of one straight"); ImGui::InputDouble("Offset tolerance (m)", &state.tolerance.offset, 0.0, 0.0, "%.3f"); + item_tooltip("Largest distance of the paths from the common axis of the straight"); ImGui::InputDouble("Treat as straight above R (m)", &state.tolerance.radius, 0.0, 0.0, "%.0f"); + item_tooltip("Curves of a larger radius count as straight"); ImGui::InputDouble("Minimum length (m)", &state.minimum_length, 0.0, 0.0, "%.0f"); + item_tooltip("Shorter straights aren't listed"); ImGui::PopItemWidth(); if (ImGui::Button("Recognize straights in the scenery")) { @@ -1649,11 +1762,6 @@ void editor_mode::render_straights_ui() state.listed = -1; state.current_for = nullptr; } - auto const describe = [](editor_track::straight const &Line) { - char text[160]; - std::snprintf(text, sizeof(text), "L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); - return std::string{text}; - }; if (false == state.found.empty()) { ImGui::Text("%zu straights", state.found.size()); @@ -1685,11 +1793,6 @@ void editor_mode::render_straight_ui() { auto &state{m_straights}; auto const &line{current_straight()}; - auto const describe = [](editor_track::straight const &Line) { - char text[160]; - std::snprintf(text, sizeof(text), "L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); - return std::string{text}; - }; if (line.tracks.empty()) { ImGui::TextDisabled(selected_track() != nullptr ? "The selected path isn't straight" : "No path selected"); @@ -1771,7 +1874,9 @@ void editor_mode::render_straight_ui() ImGui::TextDisabled(state.tool_placed ? "Drag the handle: sideways sets the shift, along sets its length" : "LMB on the straight: place the start of the shift"); ImGui::PushItemWidth(100.0f); ImGui::InputDouble("Curve R (m, 0: automatic)", &state.curve_radius, 0.0, 0.0, "%.0f"); + item_tooltip("Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line"); ImGui::Checkbox("Transitions from the line speed", &state.auto_transitions); + item_tooltip("Lengths of the transition curves from the speed of the paths and the limits of the curve design"); if (false == state.auto_transitions) { ImGui::SameLine(); @@ -1810,19 +1915,13 @@ bool editor_mode::pick_straight_handle() auto const &line{current_straight()}; glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; screen_projection const projection; - ImVec2 const mouse = ImGui::GetIO().MousePos; float best = kHandleRadius * kHandleRadius; int hit{-1}; for (int i = 0; i < 3; ++i) { - ImVec2 screen; - if (false == projection.project(handles[i], screen)) - continue; - float const dx = screen.x - mouse.x; - float const dy = screen.y - mouse.y; - if (dx * dx + dy * dy < best) + if (auto const distance{projection.mouse_distance2(handles[i])}; distance < best) { - best = dx * dx + dy * dy; + best = distance; hit = i; } } @@ -1895,22 +1994,13 @@ void editor_mode::render_straight_gizmo() return; } - ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(Global.EditorOrtho); - ImGuiIO const &io = ImGui::GetIO(); - ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); - glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); - glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); - float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - glm::mat4 const projection = editor_mode::projection_matrix(aspect); + gizmo_frame const frame; glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; auto const anchor{anchors[state.handle]}; if (false == state.dragging) - state.gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - camerapos)); - glm::vec3 snapvalue(m_gizmo_snap); - float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; - ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(state.gizmo), nullptr, snap); + state.gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, state.gizmo, m_gizmo_snap); if (ImGuizmo::IsUsing()) { @@ -1942,7 +2032,7 @@ void editor_mode::render_straight_gizmo() } } auto const &grabbed{state.drag_line}; - glm::dvec3 moved{camerapos + glm::dvec3(state.gizmo[3])}; + glm::dvec3 moved{frame.camera + glm::dvec3(state.gizmo[3])}; std::function transform; if (state.handle == 2) { @@ -1966,7 +2056,7 @@ void editor_mode::render_straight_gizmo() glm::dvec2 const offset{Point.x - pivot.x, Point.z - pivot.z}; auto const along{glm::dot(offset, axis)}; auto const across{axis.x * offset.y - axis.y * offset.x}; - auto const planar{glm::dvec2{pivot.x, pivot.z} + newaxis * (along * scale) + glm::dvec2{-newaxis.y, newaxis.x} * across}; + auto const planar{plan_of(pivot) + newaxis * (along * scale) + glm::dvec2{-newaxis.y, newaxis.x} * across}; return glm::dvec3{planar.x, Point.y + rise * along / std::max(1e-9, lengthbefore), planar.y}; }; } @@ -2020,7 +2110,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co }; auto line{Line}; auto const point = [&](double const Along) { - auto const planar{glm::dvec2{line.start.x, line.start.z} + line.direction * Along}; + auto const planar{plan_of(line.start) + line.direction * Along}; return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; }; std::vector spans; @@ -2029,9 +2119,8 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co for (std::size_t i = 0; i < line.tracks.size(); ++i) { auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; - auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; - auto const a{along(path.points[segment_data::point::start])}; - auto const b{along(path.points[segment_data::point::end])}; + auto const a{line.along(path.points[segment_data::point::start])}; + auto const b{line.along(path.points[segment_data::point::end])}; spans.push_back({line.tracks[i], line.paths[i], std::min(a, b), std::max(a, b), a < b}); } std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); @@ -2165,6 +2254,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co state.status = m_route.error; return; } + m_route.design.start_reserve = m_route.design.end_reserve = 0.0; for (auto &vertex : m_route.design.vertices) { vertex.radius = Radius; @@ -2185,7 +2275,8 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co state.status = m_route.result.errors.empty() ? "The curve can't be fitted" : m_route.result.errors.front(); return; } - auto const pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + editor_track::keep_heights(m_route.chain, pieces); auto created{editor_track::relay(m_route.chain, pieces)}; editor_track::commit(tracks); for (auto const &entry : splitstates) @@ -2201,7 +2292,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co std::string error; editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error); straight_refresh(); - state.status = "Done, the curves can be adjusted in the Route design tab"; + state.status = "Done, select a curve to adjust it in the Curve mode"; } double editor_mode::straight_tool_radius(editor_track::straight const &Line) const @@ -2231,7 +2322,7 @@ bool editor_mode::place_straight_tool() if (line.tracks.empty()) return false; glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; - auto const along{std::clamp(glm::dot(glm::dvec2{ground.x - line.start.x, ground.z - line.start.z}, line.direction), 0.0, line.length)}; + auto const along{std::clamp(line.along(ground), 0.0, line.length)}; glm::dvec3 const axis{line.direction.x, line.grade, line.direction.y}; state.tool_line = line; state.tool_at = along; @@ -2247,21 +2338,14 @@ bool editor_mode::place_straight_tool() void editor_mode::render_straight_tool_gizmo() { auto &state{m_straights}; - ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(Global.EditorOrtho); - ImGuiIO const &io = ImGui::GetIO(); - ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); - glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); - glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); - float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - glm::mat4 const projection = editor_mode::projection_matrix(aspect); + gizmo_frame const frame; if (false == state.tool_dragging) - state.tool_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(state.tool_handle - camerapos)); - ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, glm::value_ptr(state.tool_gizmo), nullptr, nullptr); + state.tool_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(state.tool_handle - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, state.tool_gizmo, 0.0f); if (ImGuizmo::IsUsing()) { state.tool_dragging = true; - auto const moved{camerapos + glm::dvec3(state.tool_gizmo[3])}; + auto const moved{frame.camera + glm::dvec3(state.tool_gizmo[3])}; state.tool_handle = {moved.x, state.tool_handle.y, moved.z}; } else if (state.tool_dragging) @@ -2275,7 +2359,7 @@ void editor_mode::apply_straight_tool() { auto &state{m_straights}; auto const line{state.tool_line}; - auto const offset{glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; auto const radius{straight_tool_radius(line)}; if (state.tool == 1) { @@ -2349,7 +2433,6 @@ void editor_mode::toggle_straight_set(TTrack &Track) state.set.push_back(line); m_node = &Track; ui()->set_node(m_node); - m_track_tab_request = 0; m_track_tab = track_tab::straights; } @@ -2363,8 +2446,7 @@ void editor_mode::delete_selected_track() snap.node_name = track->name(); snap.position = track->location(); snap.removed = {track}; - if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) - m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); + trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); editor_track::retire(*track); @@ -2376,32 +2458,203 @@ void editor_mode::delete_selected_track() straight_refresh(); } +std::vector editor_mode::track_key_hints() const +{ + std::vector hints; + bool gizmo{false}; + if (false == m_straights.detour.empty()) + { + char const *const steps[] = {"leave the axis", "shifted", "start back", "back on the axis"}; + auto const step{std::min(m_straights.detour.size(), 3)}; + hints = {{"Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}}; + } + else if (m_extend.active) + hints = {{"Drag", "curve through the cursor, along the end: straight"}, {"Ctrl", "straight"}, {"Release", "build"}}; + else if (m_switch.placing) + hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}}; + else if (m_switch.armed >= 0 && m_track_tab == track_tab::straights) + hints = {{"Press+drag", "on the selected straight where the switch starts"}}; + else if (m_track_tab == track_tab::straights && m_straights.current.tracks.empty()) + hints = {{"LMB", "select a straight or a curve"}}; + else if (m_track_tab == track_tab::straights) + { + hints = {{"Drag", "the ends or the middle"}, {"Ctrl+drag", "break"}, {"Shift+click x4", "shift around"}, {"Alt+click", "add to the set"}, {"Drag red ring", "new path"}, {"Del", "delete"}}; + gizmo = true; + } + else if (m_track_tab == track_tab::turnout) + { + hints = {{"Gizmo", "move the switch"}, {"W", "turn"}, {"Drag red ring", "new path"}, {"Del", "delete"}}; + gizmo = true; + } + else if (m_track_tab == track_tab::route && m_route.chain.tracks.empty()) + hints = {{"LMB", "select a curve"}}; + else if (m_track_tab == track_tab::route) + hints = {{"Drag yellow", "vertex"}, {"Drag green", "radius"}, {"Release", "apply"}, {"Esc", "release the vertex"}}; + else + { + hints = {{"LMB", "select a path or a point handle"}, {"Esc", "release the point"}, {"W", "turn"}, {"Drag red ring", "new path"}}; + gizmo = true; + } + if (gizmo && m_gizmo_snap > 0.0f) + hints.push_back({"Ctrl", format("gizmo snaps by %.2f m", m_gizmo_snap)}); + hints.push_back({"K", "split the path under the cursor"}); + hints.push_back({"O", "top view"}); + hints.push_back({"Ctrl+Z", "undo"}); + return hints; +} + +// values of the change in progress, shown next to the cursor +std::string editor_mode::track_readout() const +{ + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) { + auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))}; + return degrees < 0.0 ? degrees + 360.0 : degrees; + }; + if (m_extend.active) + { + double length{0.0}, radius{0.0}; + for (auto const &piece : extend_pieces()) + { + length += bezier{piece}.plan_length(); + if (piece.radius != 0.0f && (radius == 0.0 || std::abs(piece.radius) < radius)) + radius = std::abs(piece.radius); + } + auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)}; + editor_track::snap_target target; + if (extend_snap(target) && target.track != nullptr) + text += "\njoins " + name(target.track); + return text; + } + auto const &state{m_straights}; + if (state.tool_placed && state.tool_dragging) + { + auto const &line{state.tool_line}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; + if (state.tool == 1) + { + if (glm::length(offset) < 1e-3) + return {}; + auto const turn{glm::degrees(signed_angle(line.direction, glm::normalize(offset)))}; + return format("turn %+.3f deg R %.0f m", turn, straight_tool_radius(line)); + } + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + return format("shift %+.3f m over %.2f m", glm::dot(offset, normal), glm::dot(offset, line.direction)); + } + if (state.dragging && state.fitting) + { + auto const &result{m_route.result}; + if (false == result.valid) + return result.errors.empty() ? std::string{"no curve fits"} : result.errors.front(); + auto text{format("curve L %.2f m", result.length)}; + for (auto const &curve : result.curves) + text += format(" deflection %.3f deg", glm::degrees(curve.deflection)); + return text; + } + if (state.dragging) + { + auto const &grabbed{state.drag_line}; + if (state.handle == 2) + { + glm::dvec2 const normal{-grabbed.direction.y, grabbed.direction.x}; + return format("shift %+.3f m", glm::dot(plan_of(state.preview_start - grabbed.start), normal)); + } + return format("L %.3f m (%+.3f) azimuth %.4f deg", plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length, + azimuth(state.preview_start, state.preview_end)); + } + auto const *track{selected_track()}; + if (m_track_gizmo_using && track != nullptr) + { + if (m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size())) + { + auto const &path{track->m_paths[m_track_point.path]}; + auto text{format("L %.3f m", bezier{path}.plan_length())}; + if (path.radius != 0.0f) + text += format(" R %.1f m", std::abs(path.radius)); + if (m_track_snap.track != nullptr) + text += "\njoins " + name(m_track_snap.track); + return text; + } + if (m_gizmo_op != gizmo_operation::rotate) + return format("moved %.3f m", glm::distance(editor_track::pivot(*track), m_track_pivot)); + } + return {}; +} + void editor_mode::draw_track_hints() { - std::string hint; - if (false == m_straights.detour.empty()) - hint = "Shift+click: point " + std::to_string(m_straights.detour.size() + 1) + " of 4 (1: leave the axis, 2: shifted, 3: start back, 4: back on the axis) Esc: cancel"; - else if (m_extend.active) - hint = "Drag: curve through the cursor, along the end: straight Ctrl: straight"; - else if (m_switch.placing) - hint = "Drag along the straight to set the direction of the switch, sideways to set its side"; - else if (m_switch.armed >= 0 && m_track_tab == track_tab::straights) - hint = "Press on the selected straight where the switch starts and drag"; - else if (m_track_tab == track_tab::straights) - hint = current_straight().tracks.empty() ? "LMB: select a straight or a curve" : "Drag the ends or the middle Ctrl+drag: break Shift+click x4: shift around Alt+click: add to the set Drag from a red ring: new path Del: delete"; - else if (m_track_tab == track_tab::turnout) - hint = "Gizmo: move the switch (W: turn) Drag from a red ring: new path Del: delete"; - else if (m_track_tab == track_tab::route) - hint = m_route.chain.tracks.empty() ? "LMB: select a curve" : "Drag the vertex (yellow) or the radius grip (green), the change is applied on release Ctrl+Z: undo"; - else - hint = "LMB: select a path or a point handle"; - hint += " K: split the path under the cursor O: top view"; ImGuiIO const &io = ImGui::GetIO(); auto *drawlist{ImGui::GetBackgroundDrawList()}; - ImVec2 const position{12.0f, io.DisplaySize.y - 28.0f}; - auto const size{ImGui::CalcTextSize(hint.c_str())}; - drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f); - drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str()); + float const margin{12.0f}; + float const gap{14.0f}; + float const pad{4.0f}; + float const lineheight{ImGui::GetTextLineHeight() + 2.0f * pad + 4.0f}; + auto const chip = [&](ImVec2 const At, char const *Text, ImU32 const Background, ImU32 const Foreground) { + auto const size{ImGui::CalcTextSize(Text)}; + drawlist->AddRectFilled(ImVec2(At.x, At.y - 2.0f), ImVec2(At.x + size.x + 2.0f * pad, At.y + size.y + 2.0f), Background, 3.0f); + drawlist->AddText(ImVec2(At.x + pad, At.y), Foreground, Text); + return size.x + 2.0f * pad; + }; + auto const width_of = [&](key_hint const &Hint) { return ImGui::CalcTextSize(Hint.key).x + 2.0f * pad + 5.0f + ImGui::CalcTextSize(Hint.action.c_str()).x; }; + + char const *const modifiers[] = {"Ctrl", "Shift", "Alt"}; + bool const held[] = {io.KeyCtrl, io.KeyShift, io.KeyAlt}; + float modifierswidth{0.0f}; + for (auto const *modifier : modifiers) + modifierswidth += ImGui::CalcTextSize(modifier).x + 2.0f * pad + 4.0f; + + auto const hints{track_key_hints()}; + auto const available{io.DisplaySize.x - 2.0f * margin - modifierswidth - gap}; + std::vector> lines(1); + float used{0.0f}; + for (auto const &hint : hints) + { + auto const width{width_of(hint)}; + if (false == lines.back().empty() && used + gap + width > available) + { + lines.emplace_back(); + used = 0.0f; + } + used += (lines.back().empty() ? 0.0f : gap) + width; + lines.back().push_back(&hint); + } + auto y{io.DisplaySize.y - margin - lineheight * static_cast(lines.size()) + pad}; + for (std::size_t i = 0; i < lines.size(); ++i, y += lineheight) + { + float width{0.0f}; + for (auto const *hint : lines[i]) + width += (width > 0.0f ? gap : 0.0f) + width_of(*hint); + if (i + 1 == lines.size()) + width += gap + modifierswidth; + drawlist->AddRectFilled(ImVec2(margin - 6.0f, y - pad - 2.0f), ImVec2(margin + width + 6.0f, y + lineheight - pad - 2.0f), IM_COL32(0, 0, 0, 160), 4.0f); + auto x{margin}; + for (auto const *hint : lines[i]) + { + if (x > margin) + x += gap; + x += chip(ImVec2(x, y), hint->key, IM_COL32(255, 210, 60, 220), IM_COL32(20, 20, 20, 255)) + 5.0f; + drawlist->AddText(ImVec2(x, y), IM_COL32(255, 255, 255, 230), hint->action.c_str()); + x += ImGui::CalcTextSize(hint->action.c_str()).x; + } + if (i + 1 == lines.size()) + { + x += gap; + for (int m = 0; m < 3; ++m) + x += chip(ImVec2(x, y), modifiers[m], held[m] ? overlay_color::selected : IM_COL32(255, 255, 255, 40), held[m] ? IM_COL32(20, 20, 20, 255) : IM_COL32(255, 255, 255, 120)) + 4.0f; + } + } + + auto const readout{track_readout()}; + if (readout.empty() || ImGui::GetIO().WantCaptureMouse) + return; + auto *foreground{ImGui::GetForegroundDrawList()}; + auto const size{ImGui::CalcTextSize(readout.c_str())}; + ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y + 20.0f}; + at.x = std::min(at.x, io.DisplaySize.x - size.x - 10.0f); + at.y = std::min(at.y, io.DisplaySize.y - size.y - 10.0f); + foreground->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(0, 0, 0, 190), 4.0f); + foreground->AddRect(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), overlay_color::marked, 4.0f); + foreground->AddText(at, IM_COL32(255, 255, 255, 255), readout.c_str()); } void editor_mode::update_build_tools() @@ -2420,7 +2673,6 @@ bool editor_mode::start_extend() if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) return false; screen_projection const projection; - ImVec2 const mouse = ImGui::GetIO().MousePos; for (int i = 0; i < static_cast(track->m_paths.size()); ++i) { auto const &path{track->m_paths[i]}; @@ -2430,10 +2682,7 @@ bool editor_mode::start_extend() if (editor_track::is_connected(*track, point)) continue; auto const position{editor_track::point_position(*track, point)}; - ImVec2 screen; - if (false == projection.project(position, screen)) - continue; - if ((screen.x - mouse.x) * (screen.x - mouse.x) + (screen.y - mouse.y) * (screen.y - mouse.y) > kHandleRadius * kHandleRadius * 1.5f) + if (projection.mouse_distance2(position) > kHandleRadius * kHandleRadius * 1.5f) continue; auto const &start{path.points[segment_data::point::start]}; auto const &end{path.points[segment_data::point::end]}; @@ -2484,7 +2733,7 @@ std::vector editor_mode::extend_pieces() const design.start = tool.point; design.end = target.position; design.start_direction = tool.direction; - design.end_direction = glm::normalize(glm::dvec2{target.direction.x, target.direction.z}); + design.end_direction = glm::normalize(plan_of(target.direction)); auto const speed{editor_track::velocity(*tool.track)}; design.speed = speed > 0.0 ? speed : 100.0; if (false == alignment::collinear(design)) @@ -2500,7 +2749,7 @@ std::vector editor_mode::extend_pieces() const } else { - vertex.offset = glm::distance(glm::dvec2{design.start.x, design.start.z}, glm::dvec2{design.end.x, design.end.z}) / 3.0; + vertex.offset = plan_distance(design.start, design.end) / 3.0; design.vertices = {vertex, vertex}; } } @@ -2529,24 +2778,19 @@ std::vector editor_mode::extend_pieces() const auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; auto const radius{chord / (2.0 * std::sin(angle * 0.5))}; double const side{lateral > 0.0 ? 1.0 : -1.0}; - glm::dvec2 const centre{glm::dvec2{tool.point.x, tool.point.z} + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + glm::dvec2 const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)}; auto const count{std::max(1, static_cast(std::ceil(angle / glm::radians(90.0) - 1e-9)))}; auto const step{angle / count}; auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * radius}; - auto const turn = [&](glm::dvec2 const &Vector, double const Angle) { - auto const c{std::cos(Angle)}; - auto const s{std::sin(Angle)}; - return glm::dvec2{Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; - }; - glm::dvec2 const radial{glm::dvec2{tool.point.x, tool.point.z} - centre}; + glm::dvec2 const radial{plan_of(tool.point) - centre}; for (int i = 0; i < count; ++i) { auto const a0{side * step * i}; auto const a1{side * step * (i + 1)}; - auto const p0{centre + turn(radial, a0)}; - auto const p3{centre + turn(radial, a1)}; - auto const d0{turn(direction, a0)}; - auto const d3{turn(direction, a1)}; + auto const p0{centre + turned(radial, a0)}; + auto const p3{centre + turned(radial, a1)}; + auto const d0{turned(direction, a0)}; + auto const d3{turned(direction, a1)}; auto const y0{height(radius * step * i)}; auto const y3{height(radius * step * (i + 1))}; segment_data path; @@ -2579,8 +2823,7 @@ void editor_mode::finish_extend() editor_track::commit({insert}); created.push_back(insert); } - push_track_snapshot(std::move(states), std::move(created)); - m_history.back().removed = std::move(removed); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); return; } @@ -2657,7 +2900,7 @@ bool editor_mode::start_switch_placement() insert_double_slip(line, ground, tool.templates[tool.armed]); return true; } - tool.along = std::clamp(glm::dot(glm::dvec2{ground.x - line.start.x, ground.z - line.start.z}, line.direction), 0.0, line.length); + tool.along = std::clamp(line.along(ground), 0.0, line.length); tool.point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * tool.along; tool.mouse = ground; tool.line = line; @@ -2695,8 +2938,7 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const std::vector removed; if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed)) return; - push_track_snapshot(std::move(states), std::move(created)); - m_history.back().removed = std::move(removed); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); } @@ -2711,13 +2953,12 @@ bool editor_mode::cut_straight(editor_track::straight const &Line, double const double from, to; bool forward; }; - auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - Line.start.x, Point.z - Line.start.z}, Line.direction); }; std::vector spans; for (std::size_t i = 0; i < Line.tracks.size(); ++i) { auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; - auto const a{along(path.points[segment_data::point::start])}; - auto const b{along(path.points[segment_data::point::end])}; + auto const a{Line.along(path.points[segment_data::point::start])}; + auto const b{Line.along(path.points[segment_data::point::end])}; spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b}); } std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); @@ -2795,7 +3036,7 @@ bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, e { auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; for (auto const index : {segment_data::point::start, segment_data::point::end}) - joints.push_back(glm::dot(glm::dvec2{path.points[index].x - Line.start.x, path.points[index].z - Line.start.z}, Line.direction)); + joints.push_back(Line.along(path.points[index])); } auto const nearest = [&](double const Value) { double best{Value}; @@ -2834,10 +3075,10 @@ void editor_mode::draw_build_overlay() const screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); auto const drawpath = [&](segment_data const &Path, ImU32 const Color) { - auto previous{path_point(Path, 0.0)}; + auto previous{bezier{Path}.point(0.0)}; for (int i = 1; i <= 24; ++i) { - auto const next{path_point(Path, i / 24.0)}; + auto const next{bezier{Path}.point(i / 24.0)}; projection.line(drawlist, previous, next, Color, 3.0f); previous = next; } @@ -2852,33 +3093,33 @@ void editor_mode::draw_build_overlay() const if (projection.project(target.position, screen)) drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); for (auto const &piece : extend_pieces()) - drawpath(piece, IM_COL32(255, 210, 60, 255)); + drawpath(piece, overlay_color::marked); } else if (plan_crossover(plan)) { for (auto const &piece : plan.paths) - drawpath(piece, IM_COL32(255, 210, 60, 255)); - drawpath(plan.insert, IM_COL32(60, 230, 90, 255)); + drawpath(piece, overlay_color::marked); + drawpath(plan.insert, overlay_color::grip); } else { for (auto const &piece : extend_pieces()) - drawpath(piece, IM_COL32(255, 210, 60, 255)); + drawpath(piece, overlay_color::marked); } } if (m_switch.placing) for (auto const &piece : switch_preview()) - drawpath(piece, IM_COL32(255, 210, 60, 255)); + drawpath(piece, overlay_color::marked); if (false == m_straights.detour.empty()) { auto const outline{detour_outline()}; for (std::size_t i = 0; i + 1 < outline.size(); ++i) - projection.line(drawlist, outline[i], outline[i + 1], IM_COL32(255, 210, 60, 255), 3.0f); + projection.line(drawlist, outline[i], outline[i + 1], overlay_color::marked, 3.0f); for (std::size_t i = 0; i < outline.size() && i < m_straights.detour.size(); ++i) { ImVec2 screen; if (projection.project(outline[i], screen)) - drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(255, 60, 60, 255)); + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::invalid); } } auto const *track{selected_track()}; @@ -2926,6 +3167,7 @@ void editor_mode::render_switch_ui() tool.collected = false; tool.armed = -1; } + item_tooltip("Looks for the switches of the scenery again, to use their shapes as the templates"); } std::vector editor_mode::detour_outline() const @@ -2939,14 +3181,13 @@ std::vector editor_mode::detour_outline() const auto points{state.detour}; if (points.size() < 4) points.push_back(state.detour_mouse); - auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; - auto const offset{points.size() >= 2 ? glm::dot(glm::dvec2{points[1].x - line.start.x, points[1].z - line.start.z}, normal) : 0.0}; + auto const offset{points.size() >= 2 ? glm::dot(plan_of(points[1] - line.start), normal) : 0.0}; auto const place = [&](double const Along, double const Lateral) { - auto const planar{glm::dvec2{line.start.x, line.start.z} + line.direction * Along + normal * Lateral}; + auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; }; for (std::size_t i = 0; i < points.size(); ++i) - result.push_back(place(along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); + result.push_back(place(line.along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); return result; } @@ -2975,9 +3216,8 @@ void editor_mode::apply_detour() if (line.tracks.empty()) return; glm::dvec2 const normal{-line.direction.y, line.direction.x}; - auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); }; - auto const offset{glm::dot(glm::dvec2{state.detour[1].x - line.start.x, state.detour[1].z - line.start.z}, normal)}; - std::array stations{along(state.detour[0]), along(state.detour[1]), along(state.detour[2]), along(state.detour[3])}; + auto const offset{glm::dot(plan_of(state.detour[1] - line.start), normal)}; + std::array stations{line.along(state.detour[0]), line.along(state.detour[1]), line.along(state.detour[2]), line.along(state.detour[3])}; if (stations[0] > stations[3]) { std::swap(stations[0], stations[3]); @@ -3009,8 +3249,8 @@ void editor_mode::apply_detour() for (std::size_t i = 0; i < line.tracks.size(); ++i) { auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; - auto const a{along(path.points[segment_data::point::start])}; - auto const b{along(path.points[segment_data::point::end])}; + auto const a{line.along(path.points[segment_data::point::start])}; + auto const b{line.along(path.points[segment_data::point::end])}; spans.push_back({line.tracks[i], std::min(a, b), std::max(a, b)}); } std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); @@ -3039,10 +3279,10 @@ void editor_mode::apply_detour() if (m_route.chain.tracks.empty()) return; auto &design{m_route.design}; - auto const startalong{along(design.start)}; - auto const endalong{along(design.end)}; + auto const startalong{line.along(design.start)}; + auto const endalong{line.along(design.end)}; auto const place = [&](double const Along, double const Lateral) { - auto const planar{glm::dvec2{line.start.x, line.start.z} + line.direction * Along + normal * Lateral}; + auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; return planar; }; alignment::vertex vertex; @@ -3090,9 +3330,9 @@ bool editor_mode::start_curve_fit(editor_track::straight const &Line) return false; if (state.fit_before.reversals != 0 || state.fit_after.reversals != 0) return false; - auto const far_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; - m_route.from = far_end(state.fit_before, Line.start); - m_route.to = far_end(state.fit_after, Line.end); + auto const other_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; + m_route.from = other_end(state.fit_before, Line.start); + m_route.to = other_end(state.fit_after, Line.end); route_reset(); if (m_route.chain.tracks.empty()) return false; @@ -3104,8 +3344,7 @@ void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &En auto &state{m_straights}; auto &design{m_route.design}; glm::dvec2 const point{Start.x, Start.z}; - glm::dvec2 const direction{glm::normalize(glm::dvec2{End.x - Start.x, End.z - Start.z})}; - auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; + glm::dvec2 const direction{glm::normalize(plan_of(End - Start))}; glm::dvec2 const start{design.start.x, design.start.z}; glm::dvec2 const end{design.end.x, design.end.z}; auto const startdirection{glm::normalize(design.start_direction)}; @@ -3146,20 +3385,18 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const auto const &own{*tool.track}; auto const &main{own.m_paths[0]}; auto const &diverging{own.m_paths[1]}; - auto const plan = [](glm::dvec3 const &Point) { return glm::dvec2{Point.x, Point.z}; }; - auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; auto const origin{main.points[segment_data::point::start]}; - auto const axis{glm::normalize(plan(main.points[segment_data::point::end] - origin))}; + auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; glm::dvec2 const perpendicular{-axis.y, axis.x}; auto const &frog{diverging.points[segment_data::point::end]}; - auto const lateral{glm::dot(plan(frog - origin), perpendicular)}; + auto const lateral{glm::dot(plan_of(frog - origin), perpendicular)}; if (std::abs(lateral) < 0.01) return false; auto const side{lateral > 0.0 ? 1.0 : -1.0}; auto const normal{perpendicular * side}; auto const offset{std::abs(lateral)}; auto const &control{diverging.points[segment_data::point::control2]}; - auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan(control) : plan(frog - origin))}; + auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))}; auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))}; if (angle < 1e-4) return false; @@ -3177,11 +3414,11 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const if (track == &own || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) continue; auto const &path{track->m_paths.front()}; - auto const a{plan(path.points[segment_data::point::start])}; - auto const b{plan(path.points[segment_data::point::end])}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const b{plan_of(path.points[segment_data::point::end])}; auto const segment{b - a}; - auto const t{std::clamp(glm::dot(plan(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; - auto const distance{glm::distance(plan(ground), a + segment * t)}; + auto const t{std::clamp(glm::dot(plan_of(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(ground), a + segment * t)}; if (distance < best) { best = distance; @@ -3196,16 +3433,16 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const auto const &line{Plan.target}; if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) return false; - auto const distance{glm::dot(plan(line.start) - plan(origin), normal)}; + auto const distance{glm::dot(plan_of(line.start) - plan_of(origin), normal)}; auto const remaining{distance - 2.0 * offset}; if (remaining < 0.01) return false; auto const length{remaining / std::sin(angle)}; glm::dvec2 const crossing{axis * std::cos(angle) + normal * std::sin(angle)}; - auto const alongfrog{glm::dot(plan(frog - origin), axis)}; - auto const otherfrog{plan(frog) + crossing * length}; + auto const alongfrog{glm::dot(plan_of(frog - origin), axis)}; + auto const otherfrog{plan_of(frog) + crossing * length}; auto const otherorigin{otherfrog + axis * alongfrog + normal * offset}; - Plan.along = glm::dot(otherorigin - plan(line.start), line.direction); + Plan.along = glm::dot(otherorigin - plan_of(line.start), line.direction); if (Plan.along < 0.0 || Plan.along > line.length) return false; Plan.direction = glm::dot(-axis, line.direction) > 0.0 ? 1 : -1; @@ -3214,13 +3451,13 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const editor_track::switch_template shape; shape.source = tool.track; - shape.length = glm::dot(plan(main.points[segment_data::point::end] - origin), axis); + shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); for (int i = 0; i < 2; ++i) { auto const &path{own.m_paths[i]}; auto &local{shape.local[i]}; local = path; - auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan(Vector), perpendicular) * side, Vector.y, glm::dot(plan(Vector), axis)}; }; + auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), perpendicular) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; local.points[segment_data::point::start] = tolocal(path.points[segment_data::point::start] - origin); local.points[segment_data::point::end] = tolocal(path.points[segment_data::point::end] - origin); local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]); @@ -3239,8 +3476,6 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style) { - auto const plan = [](glm::dvec3 const &Vector) { return glm::dvec2{Vector.x, Vector.z}; }; - auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; }; auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) { segment_data path; @@ -3250,18 +3485,13 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g }; double const gap{0.25}; auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0}; - auto const turn = [](glm::dvec2 const &Vector, double const Rotation) { - auto const c{std::cos(Rotation)}; - auto const s{std::sin(Rotation)}; - return glm::dvec2{Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; - }; auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) { - glm::dvec2 const centre{plan(Start) + glm::dvec2{-Heading.y, Heading.x} * (Turn * Radius)}; - auto const radial{plan(Start) - centre}; - auto const p0{centre + turn(radial, Turn * From)}; - auto const p3{centre + turn(radial, Turn * To)}; - auto const d0{turn(Heading, Turn * From)}; - auto const d3{turn(Heading, Turn * To)}; + glm::dvec2 const centre{plan_of(Start) + glm::dvec2{-Heading.y, Heading.x} * (Turn * Radius)}; + auto const radial{plan_of(Start) - centre}; + auto const p0{centre + turned(radial, Turn * From)}; + auto const p3{centre + turned(radial, Turn * To)}; + auto const d0{turned(Heading, Turn * From)}; + auto const d3{turned(Heading, Turn * To)}; auto const handle{4.0 / 3.0 * std::tan((To - From) / 4.0) * Radius}; auto const y0{Start.y + (Finish.y - Start.y) * From / Angle}; auto const y3{Start.y + (Finish.y - Start.y) * To / Angle}; @@ -3292,7 +3522,7 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr; ++i) base = "editor_dks" + std::to_string(i); editor_track::switch_template shape; - shape.length = glm::distance(plan(PointA), Crossing); + shape.length = glm::distance(plan_of(PointA), Crossing); std::vector parts; parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a")); parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), reversed(arc(PointC, Second, -side, PointB, middle + split, Angle))}, Style, base + "_b")); @@ -3310,14 +3540,12 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv { auto &state{m_straights}; state.status.clear(); - auto const plan = [](glm::dvec3 const &Vector) { return glm::dvec2{Vector.x, Vector.z}; }; - auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; { auto const sections{simulation::Region->sections(Point, 40.f)}; for (auto *section : sections) for (auto const &cell : section->m_cells) for (auto *track : cell.m_paths) - if (track->eType == tt_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan(track->location()), plan(Point)) < 25.0) + if (track->eType == tt_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan_of(track->location()), plan_of(Point)) < 25.0) return replace_double_slip(*track, Shape); } TTrack *other{nullptr}; @@ -3333,19 +3561,19 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv if (std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end() || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) continue; auto const &path{track->m_paths.front()}; - auto const a{plan(path.points[segment_data::point::start])}; - auto const b{plan(path.points[segment_data::point::end])}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const b{plan_of(path.points[segment_data::point::end])}; auto const segment{b - a}; auto const determinant{cross(Line.direction, segment)}; if (std::abs(determinant) < 1e-9) continue; - auto const offset{a - plan(Line.start)}; + auto const offset{a - plan_of(Line.start)}; auto const along{cross(offset, segment) / determinant}; auto const fraction{cross(offset, Line.direction) / determinant}; if (fraction < -0.01 || fraction > 1.01 || along < 0.0 || along > Line.length) continue; - auto const position{plan(Line.start) + Line.direction * along}; - auto const distance{glm::distance(position, plan(Point))}; + auto const position{plan_of(Line.start) + Line.direction * along}; + auto const distance{glm::distance(position, plan_of(Point))}; if (distance < best) { best = distance; @@ -3373,8 +3601,8 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv state.status = "The straights cross at an angle unsuitable for a double slip"; return false; } - auto const alongfirst{glm::dot(crossing - plan(Line.start), Line.direction)}; - auto const alongsecond{glm::dot(crossing - plan(second.start), second.direction)}; + auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; + auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)}; auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; auto radius{Shape.radius}; auto half{radius * std::tan(angle * 0.5)}; @@ -3406,7 +3634,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style)) return false; auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)}; - auto const secondalong{glm::dot(crossing - plan(secondline.start), secondline.direction)}; + auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)}; if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr)) { for (auto *track : created) @@ -3426,8 +3654,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv auto const parts{build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, radius, style != nullptr ? *style : *Line.tracks.front())}; editor_track::commit(parts); created.insert(created.end(), parts.begin(), parts.end()); - push_track_snapshot(std::move(states), std::move(created)); - m_history.back().removed = std::move(removed); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); return true; } @@ -3435,8 +3662,6 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) { auto &state{m_straights}; - auto const plan = [](glm::dvec3 const &Vector) { return glm::dvec2{Vector.x, Vector.z}; }; - auto const cross = [](glm::dvec2 const &A, glm::dvec2 const &B) { return A.x * B.y - A.y * B.x; }; auto const base{Part.name().substr(0, Part.name().size() - 2)}; std::array switches{}; for (int i = 0; i < 4; ++i) @@ -3466,7 +3691,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat if (straight == nullptr) straight = &track->m_paths.front(); auto const start{straight->points[segment_data::point::start]}; - ends.push_back({track, start, glm::normalize(plan(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]}); + ends.push_back({track, start, glm::normalize(plan_of(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]}); for (int i = 0; i < 2; ++i) { auto *inner{track->SwitchExtension->pNexts[i]}; @@ -3498,10 +3723,10 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat auto const determinant{cross(first, direction)}; if (std::abs(determinant) < 1e-9) return false; - auto const t{cross(plan(c.point) - plan(a.point), direction) / determinant}; - glm::dvec2 const crossing{plan(a.point) + first * t}; + auto const t{cross(plan_of(c.point) - plan_of(a.point), direction) / determinant}; + glm::dvec2 const crossing{plan_of(a.point) + first * t}; auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; - auto const oldhalf{glm::distance(plan(a.point), crossing)}; + auto const oldhalf{glm::distance(plan_of(a.point), crossing)}; auto radius{Shape.radius}; auto half{radius * std::tan(angle * 0.5)}; auto const height{(a.point.y + b.point.y) * 0.5}; @@ -3521,8 +3746,8 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat break; } auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)}; - auto const reach{std::abs(glm::dot(plan(line.end) - crossing, -e->inward))}; - auto const reachstart{std::abs(glm::dot(plan(line.start) - crossing, -e->inward))}; + auto const reach{std::abs(glm::dot(plan_of(line.end) - crossing, -e->inward))}; + auto const reachstart{std::abs(glm::dot(plan_of(line.start) - crossing, -e->inward))}; if (std::max(reach, reachstart) - 0.5 < half) { half = std::min(half, std::max(reach, reachstart) - 0.5); @@ -3556,8 +3781,8 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat else if (half > oldhalf + 0.01 && e.outer != nullptr) { auto const line{editor_track::find_straight(*e.outer, m_straights.tolerance)}; - auto const alongold{glm::dot(plan(e.point) - plan(line.start), line.direction)}; - auto const alongnew{glm::dot(target - plan(line.start), line.direction)}; + auto const alongold{glm::dot(plan_of(e.point) - plan_of(line.start), line.direction)}; + auto const alongnew{glm::dot(target - plan_of(line.start), line.direction)}; cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr); } } @@ -3568,8 +3793,7 @@ bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_templat commitlist.push_back(track); editor_track::commit(commitlist); created.insert(created.end(), parts2.begin(), parts2.end()); - push_track_snapshot(std::move(states), std::move(created)); - m_history.back().removed = std::move(removed); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); return true; } @@ -3582,36 +3806,20 @@ std::vector editor_mode::sample_chain(editor_track::c { auto const &path{Chain.tracks[i]->m_paths.front()}; auto const forward{Chain.forward[i]}; - auto const p0{path.points[segment_data::point::start]}; - auto const p3{path.points[segment_data::point::end]}; - auto const p1{p0 + path.points[segment_data::point::control1]}; - auto const p2{p3 + path.points[segment_data::point::control2]}; - bool const straight{path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}}; - auto const length{glm::distance(p0, p3)}; + bezier const curve{path}; + auto const length{glm::distance(curve.p0, curve.p3)}; int const steps{std::max(8, static_cast(std::ceil(length / 0.5)))}; for (int k = (result.empty() ? 0 : 1); k <= steps; ++k) { auto const t{forward ? static_cast(k) / steps : 1.0 - static_cast(k) / steps}; - auto const u{1.0 - t}; - glm::dvec3 position, derivative; - if (straight) - { - position = glm::mix(p0, p3, t); - derivative = p3 - p0; - } - else - { - position = u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3; - derivative = 3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2); - } - if (false == forward) - derivative = -derivative; + auto const position{curve.point(t)}; + auto const derivative{forward ? curve.first(t) : -curve.first(t)}; if (false == result.empty()) - station += glm::length(glm::dvec2{position.x - result.back().position.x, position.z - result.back().position.z}); + station += plan_distance(position, result.back().position); curve_sample sample; sample.station = station; sample.position = position; - sample.tangent = glm::normalize(glm::dvec2{derivative.x, derivative.z}); + sample.tangent = glm::normalize(plan_of(derivative)); auto const roll{path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t}; sample.roll = static_cast(forward ? roll : -roll); sample.radius = path.radius; @@ -3651,8 +3859,8 @@ double editor_mode::nearest_station(std::vector const &Frame, glm: glm::dvec2 const a{Frame[i].position.x, Frame[i].position.z}; glm::dvec2 const b{Frame[i + 1].position.x, Frame[i + 1].position.z}; auto const segment{b - a}; - auto const t{std::clamp(glm::dot(glm::dvec2{Point.x, Point.z} - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; - auto const distance{glm::distance(glm::dvec2{Point.x, Point.z}, a + segment * t)}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(Point), a + segment * t)}; if (distance < best) { best = distance; @@ -3669,13 +3877,13 @@ std::vector editor_mode::curved_switch_paths(editor_track::switch_ auto const along{sample.tangent * static_cast(Direction)}; glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast(Side)}; Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x); - auto const planar{glm::dvec2{sample.position.x, sample.position.z} + across * Local.x}; + auto const planar{plan_of(sample.position) + across * Local.x}; auto const cant{glm::radians(static_cast(sample.roll) * Direction)}; auto const cone{-Local.x * Side * std::tan(cant)}; return glm::dvec3{planar.x, sample.position.y + Local.y + cone, planar.y}; }; auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) { - auto const handle{glm::distance(glm::dvec2{Start.x, Start.z}, glm::dvec2{End.x, End.z}) / 3.0}; + auto const handle{plan_distance(Start, End) / 3.0}; segment_data path; path.points[segment_data::point::start] = Start; path.points[segment_data::point::end] = End; @@ -3691,8 +3899,8 @@ std::vector editor_mode::curved_switch_paths(editor_track::switch_ auto const &control1{local.points[segment_data::point::control1]}; auto const &control2{local.points[segment_data::point::control2]}; glm::dvec2 const chord{end.x - start.x, end.z - start.z}; - auto const localstart{glm::normalize(control1 != glm::dvec3{} ? glm::dvec2{control1.x, control1.z} : chord)}; - auto const localend{glm::normalize(control2 != glm::dvec3{} ? -glm::dvec2{control2.x, control2.z} : chord)}; + auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; + auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; glm::dvec2 startdirection, enddirection; auto const p0{map(start, startdirection, localstart)}; auto const p3{map(end, enddirection, localend)}; @@ -3722,15 +3930,6 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) std::vector created; std::vector removed; std::vector candidates{tool.frame_tracks}; - auto const bezier = [](TTrack const &Track, double const T) { - auto const &path{Track.m_paths.front()}; - auto const p0{path.points[segment_data::point::start]}; - auto const p3{path.points[segment_data::point::end]}; - auto const p1{p0 + path.points[segment_data::point::control1]}; - auto const p2{p3 + path.points[segment_data::point::control2]}; - auto const u{1.0 - T}; - return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; - }; for (auto const cut : {from, to}) { auto const point{sample_at(tool.frame, cut).position}; @@ -3739,8 +3938,8 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) if (candidate->m_editorremoved || candidate->eType != tt_Normal) continue; auto const t{editor_track::nearest_parameter(*candidate, point)}; - auto const found{bezier(*candidate, t)}; - if (glm::distance(glm::dvec2{found.x, found.z}, glm::dvec2{point.x, point.z}) > 0.05) + auto const found{editor_track::point_at(*candidate, t)}; + if (plan_distance(found, point) > 0.05) continue; if (t > 0.002 && t < 0.998) { @@ -3759,7 +3958,7 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) { if (candidate->m_editorremoved) continue; - auto const middle{bezier(*candidate, 0.5)}; + auto const middle{editor_track::point_at(*candidate, 0.5)}; auto const where{nearest_station(tool.frame, middle)}; if (where > from + 0.01 && where < to - 0.01) { @@ -3774,7 +3973,6 @@ void editor_mode::insert_curved_switch(int const Direction, int const Side) editor_track::commit({track}); created.push_back(track); } - push_track_snapshot(std::move(states), std::move(created)); - m_history.back().removed = std::move(removed); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); } diff --git a/application/editorprojection.h b/application/editorprojection.h new file mode 100644 index 00000000..31825b0b --- /dev/null +++ b/application/editorprojection.h @@ -0,0 +1,111 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editormode.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" + +#include + +namespace overlay_color +{ +ImU32 constexpr marked{IM_COL32(255, 210, 60, 255)}; +ImU32 constexpr grip{IM_COL32(60, 230, 90, 255)}; +ImU32 constexpr selected{IM_COL32(40, 220, 255, 255)}; +ImU32 constexpr invalid{IM_COL32(255, 60, 60, 255)}; +ImU32 constexpr highlight{IM_COL32(255, 255, 255, 255)}; +} // namespace overlay_color + +// world to screen projection of the current camera, for overlays drawn with ImGui +class screen_projection +{ + public: + screen_projection() + { + ImGuiIO const &io = ImGui::GetIO(); + m_size = io.DisplaySize; + m_camera = GfxRenderer->Camera_Position(); + float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; + m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); + } + + glm::vec4 clip(glm::dvec3 const &Point) const + { + return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); + } + ImVec2 screen(glm::vec4 const &Clip) const + { + return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); + } + bool project(glm::dvec3 const &Point, ImVec2 &Screen) const + { + auto const c = clip(Point); + if (c.w < kNear) + return false; + Screen = screen(c); + return true; + } + void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const + { + glm::vec4 a = clip(A); + glm::vec4 b = clip(B); + if (a.w < kNear && b.w < kNear) + return; + if (a.w < kNear) + a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); + else if (b.w < kNear) + b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); + Drawlist->AddLine(screen(a), screen(b), Color, Thickness); + } + // squared distance on the screen from the mouse cursor to the point, the largest float for points behind the camera + float mouse_distance2(glm::dvec3 const &Point) const + { + ImVec2 position; + if (false == project(Point, position)) + return std::numeric_limits::max(); + auto const &mouse{ImGui::GetIO().MousePos}; + return (position.x - mouse.x) * (position.x - mouse.x) + (position.y - mouse.y) * (position.y - mouse.y); + } + + private: + static constexpr float kNear{0.1f}; + ImVec2 m_size; + glm::dvec3 m_camera; + glm::mat4 m_viewprojection; +}; + +// starts a gizmo frame over the whole display, the view and the projection are those of the current camera +struct gizmo_frame +{ + glm::mat4 view; + glm::mat4 projection; + glm::dvec3 camera; + + gizmo_frame() + { + ImGuizmo::BeginFrame(); + ImGuizmo::SetOrthographic(Global.EditorOrtho); + ImGuiIO const &io = ImGui::GetIO(); + ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + view = GfxRenderer->Camera_View_Matrix(); + camera = GfxRenderer->Camera_Position(); + projection = editor_mode::projection_matrix(io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f); + } + // moves the gizmo, placed at the point given in world space, with snapping to the step when ctrl is held + bool manipulate(ImGuizmo::OPERATION const Operation, glm::mat4 &Gizmo, float const Snap, glm::mat4 *Delta = nullptr) const + { + glm::vec3 snapvalue(Snap); + float const *snap = Global.ctrlState && Snap > 0.0f ? &snapvalue.x : nullptr; + return ImGuizmo::Manipulate(&view[0][0], &projection[0][0], Operation, ImGuizmo::WORLD, &Gizmo[0][0], Delta != nullptr ? &(*Delta)[0][0] : nullptr, snap); + } +}; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index f0c5d6c2..9dbc39ab 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -86,6 +86,18 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) m_filloptions(); 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; diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 31cfc530..0bd8d074 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -64,6 +64,11 @@ class editor_ui : public ui_layer { 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 void set_file_actions(std::function Save, std::function Export); // shows outcome of an operation in the layers window @@ -90,4 +95,5 @@ class editor_ui : public ui_layer std::function m_menuoptions; std::function m_gizmooptions; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any + bool *m_gaugewindow{nullptr}; }; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 703683ce..29a85b01 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -9,11 +9,12 @@ http: #include "stdafx.h" #include "editor/editorAlignment.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" #include #include #include -#include #include #include @@ -26,43 +27,20 @@ namespace double const kGravity{9.81}; double const kPi{3.14159265358979323846}; -double cross(glm::dvec2 const &A, glm::dvec2 const &B) -{ - return A.x * B.y - A.y * B.x; -} +using geometry::cross; +using geometry::plan_of; +using geometry::turned; glm::dvec2 perpendicular(glm::dvec2 const &Vector) { return {-Vector.y, Vector.x}; } -glm::dvec2 rotate(glm::dvec2 const &Vector, double const Angle) -{ - auto const c{std::cos(Angle)}; - auto const s{std::sin(Angle)}; - return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; -} - -glm::dvec2 plan(glm::dvec3 const &Point) -{ - return {Point.x, Point.z}; -} - double ceil_to(double const Value, double const Step) { return std::ceil(Value / Step - 1e-9) * Step; } -std::string format(char const *Format, ...) -{ - char buffer[512]; - va_list arguments; - va_start(arguments, Format); - std::vsnprintf(buffer, sizeof(buffer), Format, arguments); - va_end(arguments); - return buffer; -} - struct local_point { double x{0.0}; @@ -157,8 +135,8 @@ void element_point(element const &Element, transition_shape const Shape, double case element_kind::arc: { auto const angle{Element.turn * Distance / Element.radius}; - Position = Element.origin + rotate(Element.normal, angle); - Direction = rotate(Element.direction, angle); + Position = Element.origin + turned(Element.normal, angle); + Direction = turned(Element.direction, angle); break; } case element_kind::transition_in: @@ -267,7 +245,6 @@ recommendation recommend(double const Speed, double const Radius, limits const & auto const factor{Limits.gauge / (kGravity * 12.96)}; auto const allowance{Limits.gauge * Limits.unbalanced / kGravity}; result.radius_min = factor * Speed * Speed / (Limits.cant_max + allowance); - result.vertical_radius = std::max(Limits.vertical_min, Limits.vertical_factor * Speed * Speed); if (Radius <= 0.0) return result; result.cant_equilibrium = factor * Speed * Speed / Radius; @@ -277,14 +254,21 @@ recommendation recommend(double const Speed, double const Radius, limits const & return result; } -result compute(design const &Design) +namespace +{ + +result fit_between(design const &Design) { result r; r.shape = Design.shape; r.gauge = Design.norms.gauge; + r.start = Design.start; + r.end = Design.end; + auto const startreserve{std::max(0.0, Design.start_reserve)}; + auto const endreserve{std::max(0.0, Design.end_reserve)}; - auto const start{plan(Design.start)}; - auto const end{plan(Design.end)}; + auto const start{plan_of(Design.start)}; + auto const end{plan_of(Design.end)}; auto const startdirection{glm::normalize(Design.start_direction)}; auto const enddirection{glm::normalize(Design.end_direction)}; auto const count{Design.vertices.size()}; @@ -351,6 +335,16 @@ result compute(design const &Design) straight.length = Length; push(straight); }; + auto const starts_early = [&](double const Straight) { + if (Straight < 0.0) + r.start_extension = -Straight; + if (Straight >= -1e-4 || -Straight <= startreserve + 1e-4) + return; + if (startreserve > 0.0) + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the adjoining straight", -Straight - startreserve)); + else + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -Straight)); + }; struct fitted { @@ -441,9 +435,9 @@ result compute(design const &Design) std::vector scales(count, 1.0); for (std::size_t leg = 0; leg + 1 < polygon.size(); ++leg) { - auto const available{glm::distance(polygon[leg], polygon[leg + 1])}; + auto const available{glm::distance(polygon[leg], polygon[leg + 1]) + (leg == 0 ? startreserve : 0.0) + (leg == count ? endreserve : 0.0)}; auto const required{(leg > 0 ? fits[leg - 1].tangent_out : 0.0) + (leg < count ? fits[leg].tangent_in : 0.0)}; - if (required <= available) + if (required <= available + 1e-6) continue; auto const scale{available / required * (1.0 - 1e-9)}; if (leg > 0) @@ -534,13 +528,10 @@ result compute(design const &Design) auto const along{cross(directionout, endoffset) / cross(directionout, directionin)}; auto const curvestart{position - directionin * along}; auto const straight{glm::dot(curvestart - cursor, directionin)}; - if (straight < -1e-4) - { - if (k == 0) - r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight)); - else - r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); - } + if (k == 0) + starts_early(straight); + else if (straight < -1e-4) + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); push_straight(cursor, directionin, straight); auto const startchainage{chainage}; glm::dvec2 origin{curvestart}; @@ -564,7 +555,7 @@ result compute(design const &Design) push(piece); auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])}; origin += heading * local.x + piece.normal * local.y; - heading = rotate(heading, side * local.angle); + heading = turned(heading, side * local.angle); } vertexchainage[k] = (startchainage + chainage) * 0.5; cursor = origin; @@ -615,13 +606,10 @@ result compute(design const &Design) } auto const straight{glm::dot(curvestart - cursor, directionin)}; - if (straight < -1e-4) - { - if (k == 0) - r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight)); - else - r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); - } + if (k == 0) + starts_early(straight); + else if (straight < -1e-4) + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); push_straight(cursor, directionin, straight); element curve; @@ -644,7 +632,7 @@ result compute(design const &Design) curve.length = radius * std::max(0.0, arcangle); curve.origin = centre; curve.normal = arcstart - centre; - curve.direction = rotate(directionin, side * endin.angle); + curve.direction = turned(directionin, side * endin.angle); vertexchainage[k] = chainage + curve.length * 0.5; if (curve.length > 1e-6) push(curve); @@ -698,8 +686,15 @@ result compute(design const &Design) } auto const lastdirection{glm::normalize(polygon.back() - polygon[polygon.size() - 2])}; auto const tail{glm::dot(end - cursor, lastdirection)}; - if (tail < -1e-4) - r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + if (tail < 0.0) + r.end_extension = -tail; + if (tail < -1e-4 && -tail > endreserve + 1e-4) + { + if (endreserve > 0.0) + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the adjoining straight", count, -tail - endreserve)); + else + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + } push_straight(cursor, lastdirection, tail); r.length = chainage; @@ -716,12 +711,11 @@ result compute(design const &Design) r.vertex_chainages = vertexchainage; r.profile.push_back({0.0, Design.start.y, 0.0, 0.0}); + auto const radius{std::max(Design.norms.vertical_min, Design.norms.vertical_factor * Design.speed * Design.speed)}; for (std::size_t k = 0; k < count; ++k) { - auto const &vertex{Design.vertices[k]}; - auto const elevation{vertex.auto_elevation ? Design.start.y + (Design.end.y - Design.start.y) * vertexchainage[k] / std::max(r.length, 1e-6) : vertex.elevation}; + auto const elevation{Design.start.y + (Design.end.y - Design.start.y) * vertexchainage[k] / std::max(r.length, 1e-6)}; r.vertex_elevations.push_back(elevation); - auto const radius{vertex.vertical_radius > 0.0 ? vertex.vertical_radius : std::max(Design.norms.vertical_min, Design.norms.vertical_factor * Design.speed * Design.speed)}; if (vertexchainage[k] <= r.profile.back().chainage + 1e-3) continue; r.profile.push_back({vertexchainage[k], elevation, radius, 0.0}); @@ -734,20 +728,6 @@ result compute(design const &Design) auto const gradeafter{(r.profile[i + 1].elevation - point.elevation) / (r.profile[i + 1].chainage - point.chainage)}; point.tangent = point.radius * std::abs(gradeafter - gradebefore) / 2.0; } - for (std::size_t i = 0; i + 1 < r.profile.size(); ++i) - { - if (r.profile[i].tangent + r.profile[i + 1].tangent > r.profile[i + 1].chainage - r.profile[i].chainage + 1e-6) - r.warnings.emplace_back(format("Vertical curves overlap between chainage %.0f m and %.0f m", r.profile[i].chainage, r.profile[i + 1].chainage)); - } - if (r.profile.size() >= 2) - { - auto const first{(r.profile[1].elevation - r.profile[0].elevation) / (r.profile[1].chainage - r.profile[0].chainage)}; - auto const last{(r.profile.back().elevation - r.profile[r.profile.size() - 2].elevation) / (r.profile.back().chainage - r.profile[r.profile.size() - 2].chainage)}; - if (std::abs(first - Design.start_grade) > 0.0005) - r.warnings.emplace_back(format("Grade break at the start: %.1f per mille against %.1f of the adjoining track", first * 1000.0, Design.start_grade * 1000.0)); - if (std::abs(last - Design.end_grade) > 0.0005) - r.warnings.emplace_back(format("Grade break at the end: %.1f per mille against %.1f of the adjoining track", last * 1000.0, Design.end_grade * 1000.0)); - } if (Design.start_radius > 0.0 && Design.start_radius < 10000.0) r.warnings.emplace_back(format("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly", Design.start_radius)); if (Design.end_radius > 0.0 && Design.end_radius < 10000.0) @@ -757,10 +737,43 @@ result compute(design const &Design) return r; } +glm::dvec3 along_end(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Distance) +{ + auto const direction{glm::normalize(Direction)}; + return {Point.x + direction.x * Distance, Point.y + Grade * Distance, Point.z + direction.y * Distance}; +} + +} // namespace + +result compute(design const &Design) +{ + auto first{fit_between(Design)}; + auto const startextension{first.start_extension > 1e-4 ? first.start_extension : 0.0}; + auto const endextension{first.end_extension > 1e-4 ? first.end_extension : 0.0}; + if (false == first.errors.empty() || (startextension == 0.0 && endextension == 0.0)) + { + first.start_extension = first.end_extension = 0.0; + return first; + } + auto extended{Design}; + extended.start = along_end(Design.start, Design.start_direction, Design.start_grade, -startextension); + extended.end = along_end(Design.end, Design.end_direction, Design.end_grade, endextension); + extended.start_reserve = extended.end_reserve = 0.0; + if (extended.vertices.size() >= 2) + { + extended.vertices.front().offset += startextension; + extended.vertices.back().offset += endextension; + } + auto r{fit_between(extended)}; + r.start_extension = startextension; + r.end_extension = endextension; + return r; +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { - auto const start{plan(Design.start)}; - auto const offset{plan(Design.end) - start}; + auto const start{plan_of(Design.start)}; + auto const offset{plan_of(Design.end) - start}; auto const startdirection{glm::normalize(Design.start_direction)}; auto const enddirection{glm::normalize(Design.end_direction)}; auto const determinant{cross(startdirection, enddirection)}; @@ -776,7 +789,7 @@ bool tangent_intersection(design const &Design, glm::dvec2 &Point) bool collinear(design const &Design) { - auto const offset{plan(Design.end) - plan(Design.start)}; + auto const offset{plan_of(Design.end) - plan_of(Design.start)}; auto const startdirection{glm::normalize(Design.start_direction)}; auto const enddirection{glm::normalize(Design.end_direction)}; return std::abs(cross(startdirection, offset)) <= 0.01 && glm::dot(startdirection, enddirection) >= std::cos(1e-4) && glm::dot(startdirection, offset) > 0.0; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index f8628170..fcf6132b 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -45,9 +45,6 @@ struct vertex double transition_in{0.0}; double transition_out{0.0}; double cant{0.0}; - bool auto_elevation{true}; - double elevation{0.0}; - double vertical_radius{0.0}; bool reverse_turn{false}; bool compound{false}; double radius2{600.0}; @@ -65,6 +62,9 @@ struct design double end_grade{0.0}; double start_radius{0.0}; double end_radius{0.0}; + // length of the straight beyond the start and the end, which the curves can take over + double start_reserve{0.0}; + double end_reserve{0.0}; std::vector vertices; transition_shape shape{transition_shape::cubic_parabola}; double speed{100.0}; @@ -80,7 +80,6 @@ struct recommendation double cant{0.0}; double radius_min{0.0}; double transition{0.0}; - double vertical_radius{0.0}; }; recommendation recommend(double const Speed, double const Radius, limits const &Limits); double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits); @@ -152,6 +151,10 @@ struct result std::vector curves; std::vector profile; double length{0.0}; + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + double start_extension{0.0}; + double end_extension{0.0}; }; result compute(design const &Design); diff --git a/editor/editorFormat.hpp b/editor/editorFormat.hpp new file mode 100644 index 00000000..e22a9745 --- /dev/null +++ b/editor/editorFormat.hpp @@ -0,0 +1,26 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +// printf-style formatting into a string, for the messages and the data lines of the editor +template +std::string format(char const *Format, Args... Arguments) +{ + auto const length{std::snprintf(nullptr, 0, Format, Arguments...)}; + if (length <= 0) + return {}; + std::string text(static_cast(length) + 1, '\0'); + std::snprintf(text.data(), text.size(), Format, Arguments...); + text.resize(static_cast(length)); + return text; +} diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp new file mode 100644 index 00000000..bb78f25a --- /dev/null +++ b/editor/editorGauge.cpp @@ -0,0 +1,320 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorGauge.hpp" + +#include +#include +#include +#include + +namespace gauge +{ + +namespace +{ + +// the widening runs up ahead of the change: on the inner side from 20 m before it, on the outer side from 26 m +// to 6 m before it (figures 3 to 5 of the standard) +double constexpr inner_lead{20.0}; +double constexpr outer_lead{26.0}; +double constexpr outer_full{6.0}; + +struct side_widening +{ + double inner{0.0}; + double outer{0.0}; +}; + +// above the lower part the gauge includes the curves of R >= 250 m already +side_widening upper_widening(double const Radius) +{ + if (Radius >= 250.0) + return {}; + return {std::max(0.0, 50.0 / Radius - 0.185), std::max(0.0, 60.0 / Radius - 0.225)}; +} + +side_widening lower_widening(double const Radius) +{ + if (Radius >= 250.0) + return {3.75 / Radius, 3.75 / Radius}; + return upper_widening(Radius); +} + +// values at the points of a route, read as linear between them and continued past the ends +class field +{ +public: + field(std::vector const &Chainage, std::vector Values) : m_chainage{Chainage}, m_values{std::move(Values)}, m_integral(m_values.size(), 0.0) + { + for (std::size_t i = 1; i < m_values.size(); ++i) + m_integral[i] = m_integral[i - 1] + 0.5 * (m_values[i - 1] + m_values[i]) * (m_chainage[i] - m_chainage[i - 1]); + } + + double mean(double const From, double const To) const + { + return (integral(To) - integral(From)) / (To - From); + } + + // larger of the means over the window placed ahead of the point in both directions + double ahead(double const Chainage, double const Near, double const Far) const + { + return std::max(mean(Chainage + Near, Chainage + Far), mean(Chainage - Far, Chainage - Near)); + } + +private: + double integral(double const Chainage) const + { + if (Chainage <= m_chainage.front()) + return m_values.front() * (Chainage - m_chainage.front()); + if (Chainage >= m_chainage.back()) + return m_integral.back() + m_values.back() * (Chainage - m_chainage.back()); + auto const next{static_cast(std::upper_bound(m_chainage.begin(), m_chainage.end(), Chainage) - m_chainage.begin())}; + auto const i{next - 1}; + auto const span{m_chainage[next] - m_chainage[i]}; + auto const offset{Chainage - m_chainage[i]}; + auto const value{span > 0.0 ? m_values[i] + (m_values[next] - m_values[i]) * offset / span : m_values[i]}; + return m_integral[i] + 0.5 * (m_values[i] + value) * offset; + } + + std::vector const &m_chainage; + std::vector m_values; + std::vector m_integral; +}; + +// half of the distance between the heads of the rails +double constexpr rail_spread{0.75}; + +// the point turned by the angle of the sine about the head of the lower rail +place turn(section const &Section, place const &Point, double const Sine) +{ + auto const pivot{Section.tilt > 0.0 ? -rail_spread : rail_spread}; + auto const cosine{std::sqrt(std::max(0.0, 1.0 - Sine * Sine))}; + auto const lateral{Point.lateral - pivot}; + return {pivot + lateral * cosine - Point.height * Sine, lateral * Sine + Point.height * cosine}; +} + +// lower part common to all gauges, from the axis between the rails: the wheel zone at the running edge (717.5 mm +// from the axis) with the flangeway of the dimension "b" 58 mm wide and 38 mm deep, the dimension "a" of 150 mm +// at the rail top outside of it, then the limit installation gauge up to 380 mm +std::vector const common_lower{{0.6595, 0.055}, {0.6595, -0.038}, {0.7175, -0.038}, {0.7175, 0.0}, {0.8675, 0.0}, + {0.8675, 0.055}, {1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}}; + +// the wheel zone follows the rails, without the widening in the curves +bool wheel_zone(profile::point const &Point) +{ + return Point.height <= 0.055 + 1e-9 && Point.half_width < 1.0; +} + +// names of the kinds in the file +char const *const kind_names[]{"unified", "installation", "road"}; + +// outline from the points listed top down, in millimetres, as in the tables of the standard, over the lower part +profile installation(std::string const &Name, std::vector> const &Points) +{ + profile result{Name, kind::installation, common_lower}; + for (auto point = Points.rbegin(); point != Points.rend(); ++point) + result.outline.push_back({point->second * 0.001, point->first * 0.001}); + return result; +} + +// point of the outline on the side (+1 left, -1 right) at the section +place outline_point(profile const &Profile, section const &Section, std::size_t const Index, int const Side) +{ + auto const &point{Profile.outline[Index]}; + // the step at the top of the lower part belongs to it from below + bool const lower{point.height < lower_part - 1e-9 || (point.height < lower_part + 1e-9 && Index > 0 && Profile.outline[Index - 1].height < lower_part - 1e-9)}; + auto const i{Side > 0 ? 1 : 0}; + auto const widening{wheel_zone(point) ? 0.0 : lower ? Section.lower[i] : Section.upper[i] + Section.cant[i] * point.height / 1.5}; + auto const width{Section.base + point.half_width + widening}; + place const result{Side * width, point.height}; + return lower && Section.tilt != 0.0 ? turn(Section, result, Section.tilt) : result; +} + +// sign of the curvature at the points, from the nearest curve along the line where it's straight +std::vector curve_sides(std::vector const &Curvature) +{ + auto const side{[](double const Curvature) { return std::abs(Curvature) > 1e-6 ? (Curvature > 0.0 ? 1.0 : -1.0) : 0.0; }}; + std::vector sides(Curvature.size(), 0.0); + double last{0.0}; + for (std::size_t i = 0; i < Curvature.size(); ++i) + sides[i] = last = side(Curvature[i]) != 0.0 ? side(Curvature[i]) : last; + last = 0.0; + for (std::size_t i = Curvature.size(); i-- > 0;) + { + if (side(Curvature[i]) != 0.0) + last = side(Curvature[i]); + else if (sides[i] == 0.0) + sides[i] = last; + } + return sides; +} + +} // namespace + +profile const *choice::of(int const Category) const +{ + if ((Category & 1) != 0) + return railway; + if ((Category & 2) != 0) + return road; + return nullptr; +} + +std::vector default_profiles() +{ + // limit installation gauge up to 1170 mm, then the outline of the unified gauge + auto lower{common_lower}; + lower.insert(lower.end(), {{1.675, 1.170}, {2.000, 1.170}}); + std::vector const gpl1{{2.000, 3.050}, {1.900, 3.850}, {1.800, 4.250}, {1.600, 4.500}, {1.450, 4.632}}; + std::vector const gpl2{{2.000, 3.625}, {1.920, 4.900}}; + std::vector const pantograph{{1.450, 6.600}, {1.150, 6.900}, {0.0, 6.900}}; + auto const make{[&](std::string const &Name, std::vector> const &Parts) { + profile result{Name, kind::unified, lower}; + for (auto const &part : Parts) + result.outline.insert(result.outline.end(), part.begin(), part.end()); + return result; + }}; + // tables 3, 5, 7, 9 of the type cards and the drawing of GC, above 380 mm + return {make("GPL-1", {gpl1, {{1.260, 4.800}, {1.180, 4.850}, {0.0, 4.850}}}), + make("GPL-1 + pantograf", {gpl1, pantograph}), + make("GPL-2", {gpl2, {{0.0, 4.900}}}), + make("GPL-2 + pantograf", {gpl2, {{1.450, 4.900}}, pantograph}), + installation("G1 graniczna (GSZ)", {{4375, 0}, {4375, 666}, {4010, 1255}, {3700, 1550}, {3250, 1755}, {1170, 1695}, {1170, 1675}}), + installation("G1 nominalna (NSZ)", {{4385, 0}, {4385, 785}, {4010, 1365}, {3700, 1655}, {3250, 1860}, {1170, 1860}, {1170, 1675}}), + installation("G2 graniczna (GSZ)", {{4765, 0}, {4765, 940}, {3835, 1600}, {3530, 1765}, {1170, 1695}, {1170, 1675}}), + installation("G2 nominalna (NSZ)", {{4770, 0}, {4770, 1060}, {3835, 1710}, {3530, 1870}, {1170, 1870}, {1170, 1675}}), + installation("GA graniczna (GSZ)", {{4415, 0}, {4415, 687}, {4080, 1225}, {3808, 1490}, {3250, 1755}, {1170, 1700}, {1170, 1675}}), + installation("GA nominalna (NSZ)", {{4425, 0}, {4425, 805}, {4080, 1340}, {3880, 1600}, {3250, 1860}, {1170, 1860}, {1170, 1675}}), + installation("GB graniczna (GSZ)", {{4415, 0}, {4415, 687}, {4080, 1495}, {3250, 1755}, {1170, 1700}, {1170, 1675}}), + installation("GB nominalna (NSZ)", {{4425, 0}, {4425, 805}, {4110, 1610}, {3250, 1860}, {1170, 1860}, {1170, 1675}}), + installation("GC graniczna (GSZ)", {{4800, 0}, {4800, 1693}, {3550, 1765}, {1170, 1765}, {1170, 1675}}), + installation("GC nominalna (NSZ)", {{4820, 0}, {4820, 1815}, {3550, 1870}, {1170, 1870}, {1170, 1675}}), + {"Droga 4,70 m", kind::road, {{0.5, 0.0}, {0.5, 4.70}}}, + {"Droga 4,50 m", kind::road, {{0.5, 0.0}, {0.5, 4.50}}}}; +} + +// one outline per line: the name in quotes, the kind, then pairs of half-width and height +std::vector load_profiles(std::string const &File) +{ + std::ifstream input{File}; + std::vector profiles; + std::string line; + while (std::getline(input, line)) + { + std::istringstream stream{line}; + profile profile; + std::string kind; + if (false == static_cast(stream >> std::quoted(profile.name) >> kind)) + continue; + auto const known{std::find(std::begin(kind_names), std::end(kind_names), kind)}; + if (known == std::end(kind_names)) + continue; + profile.kind = static_cast(known - std::begin(kind_names)); + profile::point point; + while (stream >> point.half_width >> point.height) + profile.outline.push_back(point); + if (profile.outline.size() >= 2) + profiles.push_back(std::move(profile)); + } + return profiles.empty() ? default_profiles() : profiles; +} + +void save_profiles(std::string const &File, std::vector const &Profiles) +{ + std::ofstream output{File}; + for (auto const &profile : Profiles) + { + output << std::quoted(profile.name) << ' ' << kind_names[static_cast(profile.kind)]; + for (auto const &point : profile.outline) + output << ' ' << point.half_width << ' ' << point.height; + output << '\n'; + } +} + +std::vector
sections(kind const Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant) +{ + auto const count{Chainage.size()}; + std::vector
result(count); + if (count == 0 || Kind == kind::road) + return result; + // per side, the parts when it's the inner side of the curve and when it's the outer one + for (int side = 0; side < 2; ++side) + { + std::vector upperinner(count), upperouter(count), lowerinner(count), lowerouter(count), cantinner(count), cantouter(count); + for (std::size_t i = 0; i < count; ++i) + { + if (std::abs(Curvature[i]) < 1e-6) + continue; + auto const radius{1.0 / std::abs(Curvature[i])}; + bool const inner{(Curvature[i] > 0.0) == (side == 1)}; + auto const upper{Kind == kind::unified ? upper_widening(radius) : lower_widening(radius)}; + auto const lower{lower_widening(radius)}; + (inner ? upperinner : upperouter)[i] = inner ? upper.inner : upper.outer; + (inner ? lowerinner : lowerouter)[i] = inner ? lower.inner : lower.outer; + (inner ? cantinner : cantouter)[i] = Cant[i]; + } + field const fields[]{{Chainage, std::move(upperinner)}, {Chainage, std::move(upperouter)}, {Chainage, std::move(lowerinner)}, + {Chainage, std::move(lowerouter)}, {Chainage, std::move(cantinner)}, {Chainage, std::move(cantouter)}}; + for (std::size_t i = 0; i < count; ++i) + { + auto const inner{[&](field const &Field) { return Field.ahead(Chainage[i], 0.0, inner_lead); }}; + auto const outer{[&](field const &Field) { return Field.ahead(Chainage[i], outer_full, outer_lead); }}; + result[i].upper[side] = inner(fields[0]) + outer(fields[1]); + result[i].lower[side] = inner(fields[2]) + outer(fields[3]); + result[i].cant[side] = inner(fields[4]) - outer(fields[5]); + } + } + auto const sides{curve_sides(Curvature)}; + for (std::size_t i = 0; i < count; ++i) + result[i].tilt = -sides[i] * std::min(1.0, Cant[i] / 1.5); + return result; +} + +void ring(profile const &Profile, section const &Section, std::vector &Points) +{ + auto const count{Profile.outline.size()}; + Points.clear(); + for (std::size_t j = 0; j < count; ++j) + Points.push_back(outline_point(Profile, Section, j, -1)); + for (std::size_t j = count; j-- > 0;) + Points.push_back(outline_point(Profile, Section, j, 1)); +} + +double intrusion(std::vector const &Ring, place const &Point) +{ + if (Ring.size() < 3) + return 0.0; + bool inside{false}; + auto nearest{std::numeric_limits::max()}; + auto const *previous{&Ring.back()}; + for (auto const ¤t : Ring) + { + auto const lateral{current.lateral - previous->lateral}; + auto const height{current.height - previous->height}; + if ((current.height > Point.height) != (previous->height > Point.height) && Point.lateral < previous->lateral + lateral * (Point.height - previous->height) / height) + inside = false == inside; + auto const length{lateral * lateral + height * height}; + auto const t{length > 0.0 ? std::clamp(((Point.lateral - previous->lateral) * lateral + (Point.height - previous->height) * height) / length, 0.0, 1.0) : 0.0}; + nearest = std::min(nearest, std::hypot(Point.lateral - previous->lateral - lateral * t, Point.height - previous->height - height * t)); + previous = ¤t; + } + return inside ? nearest : -nearest; +} + +double widest(profile const &Profile) +{ + double result{0.0}; + for (auto const &point : Profile.outline) + result = std::max(result, point.half_width); + return result; +} + +} // namespace gauge diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp new file mode 100644 index 00000000..caae3a4c --- /dev/null +++ b/editor/editorGauge.hpp @@ -0,0 +1,95 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +// structure gauge (skrajnia budowli) of the PKP PLK technical standards, volume II: the space along the track +// which has to stay clear of the objects +namespace gauge +{ + +// how the outline widens along the track +enum class kind +{ + unified, // skrajnia budowli ujednolicona GPL: the curves of R >= 250 m are included above the lower part + installation, // graniczna or nominalna skrajnia zabudowy: widened by 3750/R in all curves + road // skrajnia drogi: measured from the edge of the carriageway, no widening +}; + +// right half of the outline on a straight track, in metres: half-width from the axis (for the roads from the edge +// of the carriageway) and height above the rail top or the road surface, listed from the bottom up; the left half +// is the mirror +struct profile +{ + std::string name; + gauge::kind kind{kind::unified}; + struct point + { + double half_width{0.0}; + double height{0.0}; + }; + std::vector outline; +}; + +// the outlines checked along the paths: the railway one on the tracks, the road one on the roads +struct choice +{ + profile const *railway{nullptr}; + profile const *road{nullptr}; + // by the category flags of the path, 1 a track and 2 a road; null when there's no outline for it + profile const *of(int Category) const; +}; + +// GPL-1 and GPL-2 over the limit installation gauge below 1170 mm, with and without the pantograph gauge, the limit +// and nominal installation gauges G1, G2, GA, GB, GC of the PKP PLK technical standards, and the road gauges +std::vector default_profiles(); +// outlines kept in the file, the defaults when there's none +std::vector load_profiles(std::string const &File); +void save_profiles(std::string const &File, std::vector const &Profiles); + +// the points of the paths in the scenery lie at the rail foot, the gauge is measured from the rail head +double constexpr rail_height{0.18}; + +// below this height the outline is the limit installation gauge, which leaves the room for the platforms +double constexpr lower_part{1.17}; + +// widening of the sides of the gauge at a place of the track, metres; index 0 is the right side, 1 the left one +struct section +{ + double base{0.0}; // added to the half-width of the outline: half of the carriageway of a road + std::array upper{}; // from the radius, above the lower part + std::array lower{}; // from the radius, in the lower part + std::array cant{}; // cant in metres, positive on the inner side of the curve and negative on the outer one + double tilt{0.0}; // sine of the slope of the plane of the rail heads, positive with the left rail raised +}; +// point of the cross-section, metres: lateral positive to the left, height above the rail top +struct place +{ + double lateral{0.0}; + double height{0.0}; +}; +// sections at the points of a route, from their chainage, curvature (1/m, positive turning left) and cant (m). +// The widening begins and ends ahead of the changes of the curvature and cant, as the vehicles enter a curve; +// the plane of the rail heads tilts with the cant, the outer rail raised +std::vector
sections(kind Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); + +// the outline across the track at the section, from the bottom right up and down to the bottom left, closed across +// the bottom. With the widening: the cant shifts the part above the lower one, the lower part turns with the plane +// of the rail heads instead +void ring(profile const &Profile, section const &Section, std::vector &Points); +// distance of the point from the ring in any direction, metres, positive inside and negative outside +double intrusion(std::vector const &Ring, place const &Point); +// largest half-width of the outline, metres +double widest(profile const &Profile); + +} // namespace gauge diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp new file mode 100644 index 00000000..5b1db3e1 --- /dev/null +++ b/editor/editorGeometry.hpp @@ -0,0 +1,131 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include "world/Segment.h" + +// plan (x, z) geometry and cubic Bezier curves of the paths, shared by the track tools +namespace geometry +{ + +// points of the paths closer than this are taken for the same point +double constexpr same_point{0.05}; + +inline glm::dvec2 plan_of(glm::dvec3 const &Point) +{ + return {Point.x, Point.z}; +} + +inline double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +// z component of the 3d cross product; for a unit Direction it's the offset of the Point to its left +inline double cross(glm::dvec2 const &Direction, glm::dvec2 const &Point) +{ + return Direction.x * Point.y - Direction.y * Point.x; +} + +inline glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle) +{ + auto const c{std::cos(Angle)}; + auto const s{std::sin(Angle)}; + return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; +} + +inline double signed_angle(glm::dvec2 const &From, glm::dvec2 const &To) +{ + return std::atan2(cross(From, To), glm::dot(From, To)); +} + +// slope of the direction, rise over the run in the plan +inline double grade_of(glm::dvec3 const &Direction) +{ + return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); +} + +// path as a cubic Bezier curve; a path without control points is a straight with evenly spread parameter +struct bezier +{ + glm::dvec3 p0, p1, p2, p3; + + explicit bezier(segment_data const &Path) + { + p0 = Path.points[segment_data::point::start]; + p3 = Path.points[segment_data::point::end]; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + p1 = glm::mix(p0, p3, 1.0 / 3.0); + p2 = glm::mix(p0, p3, 2.0 / 3.0); + } + else + { + p1 = p0 + control1; + p2 = p3 + control2; + } + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + } + glm::dvec3 first(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + } + glm::dvec3 second(double const T) const + { + return 6.0 * (1.0 - T) * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1); + } + double plan_length(int const Steps = 32) const + { + double length{0.0}; + auto previous{plan_of(p0)}; + for (int i = 1; i <= Steps; ++i) + { + auto const next{plan_of(point(static_cast(i) / Steps))}; + length += glm::distance(previous, next); + previous = next; + } + return length; + } + // parameter at specified plan distance from the start + double parameter(double const Distance, int const Steps = 256) const + { + double length{0.0}; + auto previous{plan_of(p0)}; + for (int i = 1; i <= Steps; ++i) + { + auto const next{plan_of(point(static_cast(i) / Steps))}; + auto const step{glm::distance(previous, next)}; + if (length + step >= Distance) + return (i - 1 + (step > 1e-12 ? (Distance - length) / step : 0.0)) / Steps; + length += step; + previous = next; + } + return 1.0; + } +}; + +// distance between the ends of the path, in the plan +inline double chord_length(segment_data const &Path) +{ + return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])); +} + +} // namespace geometry diff --git a/editor/editorInfra.cpp b/editor/editorInfra.cpp new file mode 100644 index 00000000..ba38961f --- /dev/null +++ b/editor/editorInfra.cpp @@ -0,0 +1,284 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorInfra.hpp" + +#include "editor/editorGeometry.hpp" +#include "world/Track.h" + +#include +#include +#include +#include + +namespace infra +{ + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +int const kSteps{128}; + +// arc length in the plan at the evenly spread parameters +std::array lengths(bezier const &Curve) +{ + std::array result{}; + auto previous{Curve.p0}; + for (int i = 1; i <= kSteps; ++i) + { + auto const next{Curve.point(static_cast(i) / kSteps)}; + result[i] = result[i - 1] + plan_distance(previous, next); + previous = next; + } + return result; +} + +double parameter(std::array const &Lengths, double const S) +{ + if (S <= 0.0) + return 0.0; + if (S >= Lengths.back()) + return 1.0; + auto const next{std::upper_bound(Lengths.begin(), Lengths.end(), S)}; + auto const index{static_cast(std::distance(Lengths.begin(), next)) - 1}; + auto const run{Lengths[index + 1] - Lengths[index]}; + auto const fraction{run > 1e-12 ? (S - Lengths[index]) / run : 0.0}; + return (index + fraction) / kSteps; +} + +double length_at(std::array const &Lengths, double const T) +{ + auto const position{std::clamp(T, 0.0, 1.0) * kSteps}; + auto const index{std::min(static_cast(position), kSteps - 1)}; + return Lengths[index] + (Lengths[index + 1] - Lengths[index]) * (position - index); +} + +glm::dvec2 plan_direction(bezier const &Curve, double const T) +{ + auto tangent{Curve.first(T)}; + glm::dvec2 result{tangent.x, tangent.z}; + if (glm::length(result) < 1e-9) + result = {Curve.p3.x - Curve.p0.x, Curve.p3.z - Curve.p0.z}; + return glm::length(result) > 1e-9 ? glm::normalize(result) : glm::dvec2{0.0, 1.0}; +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return Text; +} + +} // namespace + +char const *name(kind const Kind) +{ + switch (Kind) + { + case kind::model: return "model"; + case kind::include: return "include"; + case kind::traction: return "traction"; + case kind::memcell: return "memcell"; + case kind::launcher: return "eventlauncher"; + } + return ""; +} + +char const *name(category const Category) +{ + switch (Category) + { + case category::signal: return "Signals"; + case category::sign: return "Signs"; + case category::pole: return "Poles and gantries"; + case category::catenary: return "Catenary"; + case category::logic: return "Logic"; + case category::other: return "Other"; + } + return ""; +} + +std::vector const &categories() +{ + static std::vector const result{category::signal, category::sign, category::pole, category::catenary, category::logic, category::other}; + return result; +} + +double plan_length(segment_data const &Path) +{ + return lengths(bezier{Path}).back(); +} + +std::vector outline(segment_data const &Path, double const Step) +{ + bezier const shape{Path}; + // the speed along the curve doesn't exceed three times the longest leg of the control polygon + auto const leg{std::max({plan_distance(shape.p0, shape.p1), plan_distance(shape.p1, shape.p2), plan_distance(shape.p2, shape.p3)})}; + auto const count{std::clamp(static_cast(std::ceil(3.0 * leg / std::max(Step, 0.1))), 1, 4096)}; + std::vector result; + result.reserve(count + 1); + for (int i = 0; i <= count; ++i) + result.push_back(shape.point(static_cast(i) / count)); + return result; +} + +frame frame_at(segment_data const &Path, double const S) +{ + bezier const shape{Path}; + auto const t{parameter(lengths(shape), S)}; + return {shape.point(t), plan_direction(shape, t)}; +} + +frame frame_at(station const &Station) +{ + if (Station.track == nullptr || Station.path < 0 || Station.path >= static_cast(Station.track->m_paths.size())) + return {}; + return frame_at(Station.track->m_paths[Station.path], Station.s); +} + +double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior) +{ + bezier const shape{Path}; + auto const table{lengths(shape)}; + auto const distance = [&](double const T) { return plan_distance(shape.point(T), Point); }; + auto best{0}; + auto bestdistance{distance(0.0)}; + for (int i = 1; i <= kSteps; ++i) + { + auto const d{distance(static_cast(i) / kSteps)}; + if (d < bestdistance) + { + bestdistance = d; + best = i; + } + } + // golden section search around the nearest sample + auto low{std::max(0, best - 1) / static_cast(kSteps)}; + auto high{std::min(kSteps, best + 1) / static_cast(kSteps)}; + double const ratio{0.6180339887498949}; + auto a{high - ratio * (high - low)}; + auto b{low + ratio * (high - low)}; + auto da{distance(a)}, db{distance(b)}; + for (int i = 0; i < 40; ++i) + { + if (da < db) + { + high = b; + b = a; + db = da; + a = high - ratio * (high - low); + da = distance(a); + } + else + { + low = a; + a = b; + da = db; + b = low + ratio * (high - low); + db = distance(b); + } + } + auto t{(low + high) * 0.5}; + if (distance(0.0) <= distance(t)) + t = 0.0; + if (distance(1.0) <= distance(t)) + t = 1.0; + S = length_at(table, t); + Interior = (t > 1e-6 && t < 1.0 - 1e-6); + return distance(t); +} + +double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior) +{ + auto best{-1.0}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + double s; + bool interior; + auto const d{project(Track.m_paths[i], Point, s, interior)}; + // an interior point takes precedence over an end shared with the other path + if (best < 0.0 || d < best - 1e-6 || (std::abs(d - best) <= 1e-6 && interior && false == Interior)) + { + best = d; + Station = {&Track, i, s}; + Interior = interior; + } + } + return best; +} + +double heading(glm::dvec2 const &Forward) +{ + return glm::degrees(std::atan2(Forward.x, Forward.y)); +} + +anchor make_anchor(TTrack *Track, int const Path, glm::dvec3 const &Point) +{ + double s; + bool interior; + project(Track->m_paths[Path], Point, s, interior); + return make_anchor({Track, Path, s}, Point); +} + +anchor make_anchor(station const &At, glm::dvec3 const &Point) +{ + anchor result; + result.at = At; + auto const axis{frame_at(At)}; + glm::dvec2 const side{axis.forward.y, -axis.forward.x}; + result.offset = glm::dot(plan_of(Point - axis.point), side); + result.height = Point.y - axis.point.y; + result.foot = axis.point; + if (At.track != nullptr && At.path >= 0 && At.path < static_cast(At.track->m_paths.size())) + { + auto const &path{At.track->m_paths[At.path]}; + result.start = path.points[segment_data::point::start]; + result.end = path.points[segment_data::point::end]; + result.length = plan_length(path); + } + return result; +} + +glm::dvec3 place(anchor &Anchor) +{ + auto const axis{frame_at(Anchor.at)}; + Anchor.foot = axis.point; + glm::dvec2 const side{axis.forward.y, -axis.forward.x}; + return {axis.point.x + side.x * Anchor.offset, axis.point.y + Anchor.height, axis.point.z + side.y * Anchor.offset}; +} + +std::vector default_rules() +{ + return { + {category::signal, "semafor sem_ sem- tarcza tarcz_ tz_ tm_ tzw powtarz sygnaliz sygnal signal"}, + {category::sign, "wskaz wsk_ wsk- w4 w8 w11 w24 w27 kilometr hektometr slupek_km znak tablic sign"}, + {category::catenary, "trakcj siec_ siec- catenary przewod lina_nosna"}, + {category::pole, "slup maszt wysieg bramk konstrukcj kotw odciag pole gantry"}, + }; +} + +category classify(std::string const &Name, std::vector const &Rules) +{ + auto const text{lower(Name)}; + for (auto const &rule : Rules) + { + std::istringstream words{lower(rule.keywords)}; + std::string word; + while (words >> word) + if (text.find(word) != std::string::npos) + return rule.group; + } + return category::other; +} + +} // namespace infra diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp new file mode 100644 index 00000000..64d32b25 --- /dev/null +++ b/editor/editorInfra.hpp @@ -0,0 +1,130 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "world/Segment.h" +#include "scene/scenenode.h" + +class TTrack; + +// objects which belong to the railway infrastructure (signals, signs, catenary, poles, the logic placed along the +// line) bound to the paths they stand by, so they follow the changes of the track geometry +namespace infra +{ + +enum class kind +{ + model, + include, + traction, + memcell, + launcher +}; +enum class category +{ + signal, + sign, + pole, + catenary, + logic, + other +}; +char const *name(kind Kind); +char const *name(category Category); +std::vector const &categories(); + +// point of a path, measured in the plan from the start of the path +struct station +{ + TTrack *track{nullptr}; + int path{0}; + double s{0.0}; +}; +struct frame +{ + glm::dvec3 point{0.0}; + glm::dvec2 forward{0.0, 1.0}; // in the plan, x and z +}; + +double plan_length(segment_data const &Path); +// points of the path, no farther apart in the plan than about specified step; the ends included +std::vector outline(segment_data const &Path, double Step); +frame frame_at(segment_data const &Path, double S); +frame frame_at(station const &Station); +// nearest point of specified path in the plan. returns: distance in the plan; Interior tells the nearest point +// isn't one of the ends of the path +double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior); +// nearest point among the paths of the track +double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior); +// angle around the vertical axis which turns a model to face specified direction, in degrees +double heading(glm::dvec2 const &Forward); + +// location of a point relative to the rail axis: the station, a sideways offset and a height above the rail top +struct anchor +{ + station at; + double offset{0.0}; // positive to the right of the direction of the path + double height{0.0}; + glm::dvec3 foot{0.0}; // point of the axis the location was measured from + // ends and length of the path when the station was measured, to tell whether the path changed since + glm::dvec3 start{0.0}, end{0.0}; + double length{0.0}; +}; +anchor make_anchor(station const &At, glm::dvec3 const &Point); +// anchor at the place of the path nearest to the point +anchor make_anchor(TTrack *Track, int const Path, glm::dvec3 const &Point); +// location for the current geometry of the path +glm::dvec3 place(anchor &Anchor); + +// rules telling infrastructure apart by the names of the files and nodes +struct rule +{ + category group{category::other}; + std::string keywords; // separated by spaces, matched as parts of the lower case name +}; +std::vector default_rules(); +// first rule in the list which matches. returns: category::other if none does +category classify(std::string const &Name, std::vector const &Rules); + +struct binding +{ + kind type{kind::model}; + category group{category::other}; + scene::basic_node *node{nullptr}; // the object, unless it's an include + scene::instance_handle include{0}; + std::vector anchors; // one per point which defines the object: four for a traction piece + double yaw{0.0}; // rotation of the object relative to the direction of the path + bool turns{false}; // the object has a rotation which follows the path + std::string label; + bool lost{false}; // none of the paths around could take the object over + + bool same(binding const &Other) const + { + return type == Other.type && node == Other.node && include == Other.include; + } +}; + +// placement of a bound object, to take a change back +struct object_state +{ + binding saved; + glm::dvec3 location{0.0}; + glm::vec3 angles{0.f}; + std::array points{}; + std::string directive; + bool moved{false}; // the object followed a change since the state was taken +}; + +} // namespace infra diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp new file mode 100644 index 00000000..3ea7e13b --- /dev/null +++ b/editor/editorProfile.cpp @@ -0,0 +1,415 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorProfile.hpp" +#include "editor/editorFormat.hpp" + +#include +#include +#include +#include + +namespace profile +{ + +namespace +{ + +double polyline(line const &Line, double const Chainage) +{ + auto const &points{Line.points}; + if (points.empty()) + return 0.0; + if (points.size() == 1 || Chainage <= points.front().chainage) + return points.front().elevation + (points.size() > 1 ? grade_after(Line, 0) * (Chainage - points.front().chainage) : 0.0); + if (Chainage >= points.back().chainage) + return points.back().elevation + grade_after(Line, points.size() - 2) * (Chainage - points.back().chainage); + auto const next{std::upper_bound(points.begin(), points.end(), Chainage, [](double const Value, pvi const &Point) { return Value < Point.chainage; })}; + auto const index{static_cast(std::distance(points.begin(), next)) - 1}; + return points[index].elevation + grade_after(Line, index) * (Chainage - points[index].chainage); +} + +bool overlaps(double const From, double const To, context::zone const &Zone) +{ + return From < Zone.to - 1e-6 && To > Zone.from + 1e-6; +} + +} // namespace + +double grade_after(line const &Line, std::size_t const Index) +{ + auto const &points{Line.points}; + if (Index + 1 >= points.size()) + return 0.0; + auto const run{points[Index + 1].chainage - points[Index].chainage}; + return run > 1e-9 ? (points[Index + 1].elevation - points[Index].elevation) / run : 0.0; +} + +double required_radius(line const &Line) +{ + return std::max(Line.norms.radius_min, Line.norms.radius_factor * Line.speed * Line.speed); +} + +void assign_radii(line &Line) +{ + auto const required{std::ceil(required_radius(Line) / 10.0) * 10.0}; + for (std::size_t k = 1; k + 1 < Line.points.size(); ++k) + { + auto &point{Line.points[k]}; + if (false == point.automatic || point.joint) + continue; + auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; + point.radius = change > Line.norms.curve_threshold + 1e-6 ? required : 0.0; + } +} + +void complete(line &Line, context const &Context) +{ + auto &points{Line.points}; + points.erase(std::remove_if(points.begin(), points.end(), [](pvi const &Point) { return Point.joint; }), points.end()); + if (points.size() < 2) + return; + bool const start{Line.join_start && Context.start_joined}; + bool const end{Line.join_end && Context.end_joined}; + if (start) + points.front().elevation = Context.start_elevation; + if (end) + points.back().elevation = Context.end_elevation; + + // the vertical curve of the joint starts right at the end of the route, off the adjoining track + auto const joint = [&](pvi const &End, pvi const &Neighbour, double const Grade, double const Direction) -> std::optional { + auto const run{(Neighbour.chainage - End.chainage) * Direction}; + if (run < 1.0) + return std::nullopt; + auto const offset{std::abs(Neighbour.elevation - (End.elevation + Grade * run * Direction))}; + if (offset / run * 1000.0 <= Line.norms.curve_threshold + 1e-6) + return std::nullopt; + auto radius{std::ceil(required_radius(Line) / 10.0) * 10.0}; + double tangent; + if (run * run >= 2.0 * radius * offset) + tangent = (run - std::sqrt(run * run - 2.0 * radius * offset)) * 0.5; + else + { + tangent = run * 0.5; + radius = run * run / (2.0 * offset); + } + pvi result; + result.chainage = End.chainage + tangent * Direction; + result.elevation = End.elevation + Grade * tangent * Direction; + result.radius = radius; + result.automatic = false; + result.joint = true; + return result; + }; + if (start) + if (auto const point{joint(points.front(), points[1], Context.start_grade, 1.0)}) + points.insert(points.begin() + 1, *point); + if (end) + if (auto const point{joint(points.back(), points[points.size() - 2], Context.end_grade, -1.0)}) + points.insert(points.end() - 1, *point); + assign_radii(Line); +} + +std::vector curves(line const &Line) +{ + std::vector result; + auto const &points{Line.points}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + auto const radius{points[k].radius}; + if (radius <= 0.0) + continue; + curve c; + c.point = k; + c.change = grade_after(Line, k) - grade_after(Line, k - 1); + c.tangent = radius * std::abs(c.change) * 0.5; + if (c.tangent < 1e-6) + continue; + c.sagitta = c.tangent * c.tangent / (2.0 * radius); + c.start = points[k].chainage - c.tangent; + c.end = points[k].chainage + c.tangent; + result.push_back(c); + } + return result; +} + +double elevation(line const &Line, double const Chainage) +{ + auto height{polyline(Line, Chainage)}; + for (auto const &c : curves(Line)) + { + auto const distance{std::abs(Chainage - Line.points[c.point].chainage)}; + if (distance >= c.tangent) + continue; + height += c.change * (c.tangent - distance) * (c.tangent - distance) / (4.0 * c.tangent); + } + return height; +} + +double grade(line const &Line, double const Chainage) +{ + auto const &points{Line.points}; + if (points.size() < 2) + return 0.0; + std::size_t index{0}; + while (index + 2 < points.size() && Chainage >= points[index + 1].chainage) + ++index; + auto slope{grade_after(Line, index)}; + for (auto const &c : curves(Line)) + { + auto const offset{Chainage - points[c.point].chainage}; + if (std::abs(offset) >= c.tangent) + continue; + slope -= c.change * (c.tangent - std::abs(offset)) / (2.0 * c.tangent) * (offset < 0.0 ? -1.0 : 1.0); + } + return slope; +} + +double radius_for_sagitta(line const &Line, std::size_t const Point, double const Sagitta) +{ + if (Point == 0 || Point + 1 >= Line.points.size()) + return 0.0; + auto const change{std::abs(grade_after(Line, Point) - grade_after(Line, Point - 1))}; + if (change < 1e-9) + return Line.points[Point].radius; + return std::max(0.0, 8.0 * std::abs(Sagitta) / (change * change)); +} + +std::vector check(line const &Line, context const &Context) +{ + std::vector result; + auto const &points{Line.points}; + auto const &norms{Line.norms}; + auto const add = [&](bool const Error, int const Point, double const Chainage, std::string Text) { + result.push_back({Error, Point, Chainage, std::move(Text)}); + }; + if (points.size() < 2) + { + add(true, -1, 0.0, "The grade line needs at least the two ends"); + return result; + } + for (std::size_t k = 0; k + 1 < points.size(); ++k) + { + if (points[k + 1].chainage - points[k].chainage < 0.5) + add(true, static_cast(k + 1), points[k + 1].chainage, format("Points %zu and %zu are closer than 0.5 m", k + 1, k + 2)); + auto const slope{grade_after(Line, k) * 1000.0}; + if (std::abs(slope) > norms.grade_max + 1e-6) + add(false, static_cast(k), points[k].chainage, format("Grade %.2f per mille between points %zu and %zu exceeds %.2f", slope, k + 1, k + 2, norms.grade_max)); + } + + auto const list{curves(Line)}; + auto const required{required_radius(Line)}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; + auto const radius{points[k].radius}; + if (radius <= 0.0 && change > norms.curve_threshold + 1e-6) + add(false, static_cast(k), points[k].chainage, format("Point %zu: change of grade %.2f per mille needs a vertical curve", k + 1, change)); + else if (radius > 0.0 && radius < required - 1e-6) + add(false, static_cast(k), points[k].chainage, format("Point %zu: R %.0f m is below the required %.0f m for %.0f km/h", k + 1, radius, required, Line.speed)); + if (radius <= 0.0 && change > 1e-3) + for (auto const &zone : Context.switches) + if (points[k].chainage > zone.from + 1e-3 && points[k].chainage < zone.to - 1e-3) + add(true, static_cast(k), points[k].chainage, format("Point %zu: the grade changes inside switch %s", k + 1, zone.name.c_str())); + } + for (std::size_t i = 0; i < list.size(); ++i) + { + auto const &c{list[i]}; + auto const point{static_cast(c.point)}; + if (c.start < points.front().chainage - 1e-6 || c.end > points.back().chainage + 1e-6) + add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve reaches past the end of the route", c.point + 1)); + if (i + 1 < list.size() && c.end > list[i + 1].start + 1e-6) + add(true, point, c.end, format("Vertical curves of points %zu and %zu overlap by %.2f m", c.point + 1, list[i + 1].point + 1, c.end - list[i + 1].start)); + for (auto const &zone : Context.switches) + if (overlaps(c.start, c.end, zone)) + add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve lies on switch %s", c.point + 1, zone.name.c_str())); + } + if (norms.element_min > 0.0) + { + for (std::size_t k = 1; k + 2 < points.size(); ++k) + { + auto const tangent = [&](std::size_t const Point) { + for (auto const &c : list) + if (c.point == Point) + return c.tangent; + return 0.0; + }; + auto const free{(points[k + 1].chainage - tangent(k + 1)) - (points[k].chainage + tangent(k))}; + if (free < norms.element_min - 1e-6) + add(false, static_cast(k), points[k].chainage, format("Constant grade between points %zu and %zu is %.1f m long, less than %.0f m", k + 1, k + 2, std::max(0.0, free), norms.element_min)); + } + } + for (auto const &zone : Context.switches) + { + auto const slope{std::abs(grade(Line, (zone.from + zone.to) * 0.5)) * 1000.0}; + if (slope > norms.grade_switch_max + 1e-6) + add(false, -1, zone.from, format("Grade %.2f per mille over switch %s exceeds %.2f", slope, zone.name.c_str(), norms.grade_switch_max)); + } + for (auto const &fixed : Context.fixed) + { + auto const departure{elevation(Line, fixed.first) - fixed.second}; + if (std::abs(departure) > norms.fixed_tolerance) + { + add(true, -1, fixed.first, format("At %s the grade line departs %.3f m from a fixed point", format_chainage(fixed.first).c_str(), departure)); + break; + } + } + auto const ends = [&](bool const Atend) { + auto const joined{Atend ? Context.end_joined : Context.start_joined}; + if (false == joined) + return; + auto const chainage{Atend ? points.back().chainage : points.front().chainage}; + auto const departure{elevation(Line, chainage) - (Atend ? Context.end_elevation : Context.start_elevation)}; + if (std::abs(departure) > norms.fixed_tolerance) + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("The %s of the grade line is %.3f m off the adjoining track", Atend ? "end" : "start", departure)); + auto const slope{grade(Line, chainage)}; + auto const change{std::abs(slope - (Atend ? Context.end_grade : Context.start_grade)) * 1000.0}; + if (change > norms.curve_threshold + 1e-6) + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("Change of grade %.2f per mille against the adjoining track at the %s", change, Atend ? "end" : "start")); + }; + ends(false); + ends(true); + std::stable_sort(result.begin(), result.end(), [](issue const &A, issue const &B) { return A.error && false == B.error; }); + return result; +} + +line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options) +{ + line result; + auto const count{std::min({Chainages.size(), Elevations.size(), Grades.size()})}; + if (count < 2) + return result; + + struct part + { + std::size_t first, last; + double slope{0.0}, offset{0.0}; + }; + auto const fit = [&](part &Part) { + double sx{0.0}, sy{0.0}, sxx{0.0}, sxy{0.0}; + auto const origin{Chainages[Part.first]}; + auto const n{static_cast(Part.last - Part.first + 1)}; + for (auto i = Part.first; i <= Part.last; ++i) + { + auto const x{Chainages[i] - origin}; + sx += x; + sy += Elevations[i]; + sxx += x * x; + sxy += x * Elevations[i]; + } + auto const denominator{n * sxx - sx * sx}; + Part.slope = std::abs(denominator) > 1e-12 ? (n * sxy - sx * sy) / denominator : 0.0; + Part.offset = (sy - Part.slope * sx) / n - Part.slope * origin; + }; + + std::vector parts; + for (std::size_t i = 0; i < count;) + { + auto j{i}; + auto low{Grades[i]}, high{Grades[i]}; + while (j + 1 < count && std::max(high, Grades[j + 1]) - std::min(low, Grades[j + 1]) <= Options.grade_tolerance) + { + ++j; + low = std::min(low, Grades[j]); + high = std::max(high, Grades[j]); + } + if (Chainages[j] - Chainages[i] >= Options.minimum_length) + { + part p{i, j}; + fit(p); + parts.push_back(p); + i = j + 1; + } + else + ++i; + } + for (std::size_t i = 0; i + 1 < parts.size();) + { + if (std::abs(parts[i].slope - parts[i + 1].slope) < Options.grade_tolerance * 0.5) + { + parts[i].last = parts[i + 1].last; + fit(parts[i]); + parts.erase(parts.begin() + i + 1); + } + else + ++i; + } + + result.points.push_back({Chainages.front(), Elevations.front(), 0.0}); + for (std::size_t i = 0; i + 1 < parts.size(); ++i) + { + auto const &a{parts[i]}; + auto const &b{parts[i + 1]}; + auto const from{Chainages[a.last]}; + auto const to{Chainages[b.first]}; + auto const change{b.slope - a.slope}; + auto chainage{std::abs(change) > 1e-9 ? (a.offset - b.offset) / change : (from + to) * 0.5}; + chainage = std::clamp(chainage, from, std::max(from, to)); + pvi point; + point.automatic = false; + point.chainage = chainage; + point.elevation = a.slope * chainage + a.offset; + auto const gap{to - from}; + if (gap >= Options.sharp_gap && std::abs(change) > 1e-9) + point.radius = std::round(gap / std::abs(change)); + if (point.chainage - result.points.back().chainage >= 0.5) + result.points.push_back(point); + } + if (Chainages[count - 1] - result.points.back().chainage < 0.5 && result.points.size() > 1) + result.points.pop_back(); + result.points.push_back({Chainages[count - 1], Elevations[count - 1], 0.0}); + + // the gap between constant grades is shorter than the curve, the sagitta gives the radius + for (std::size_t i = 1; i + 1 < result.points.size(); ++i) + { + auto &point{result.points[i]}; + if (point.radius <= 0.0) + continue; + auto const next{std::lower_bound(Chainages.begin(), Chainages.begin() + count, point.chainage)}; + if (next == Chainages.begin() || next == Chainages.begin() + count) + continue; + auto const index{static_cast(std::distance(Chainages.begin(), next))}; + auto const run{Chainages[index] - Chainages[index - 1]}; + auto const measured{run > 1e-9 ? Elevations[index - 1] + (Elevations[index] - Elevations[index - 1]) * (point.chainage - Chainages[index - 1]) / run : Elevations[index]}; + auto const radius{radius_for_sagitta(result, i, measured - point.elevation)}; + if (radius > 0.0) + point.radius = std::round(radius); + } + return result; +} + +std::vector breaks(line const &Line) +{ + std::vector result; + auto const &points{Line.points}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + if (points[k].radius > 0.0) + continue; + if (std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) > 1e-7) + result.push_back(points[k].chainage); + } + for (auto const &c : curves(Line)) + { + result.push_back(c.start); + result.push_back(c.end); + } + std::sort(result.begin(), result.end()); + return result; +} + +std::string format_chainage(double const Chainage) +{ + auto const value{std::abs(Chainage)}; + auto const kilometres{static_cast(value / 1000.0)}; + return format("%s%ld+%06.2f", Chainage < 0.0 ? "-" : "", kilometres, value - kilometres * 1000.0); +} + +} diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp new file mode 100644 index 00000000..ebc1ca0b --- /dev/null +++ b/editor/editorProfile.hpp @@ -0,0 +1,119 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +// vertical alignment (grade line) of the rail top along the chainage of a route +namespace profile +{ + +// point of intersection of two grades, with a parabolic vertical curve of radius R (0: none) +struct pvi +{ + double chainage{0.0}; + double elevation{0.0}; + double radius{0.0}; + bool automatic{true}; // radius follows the limits + bool joint{false}; // placed by complete(), takes the grade over from the adjoining track +}; + +struct limits +{ + double grade_max{6.0}; // per mille + double grade_switch_max{2.5}; // per mille, over switches + double radius_factor{0.4}; // R >= k * V^2 + double radius_min{2000.0}; + double curve_threshold{2.0}; // per mille, change of grade which needs a vertical curve + double element_min{0.0}; // m, constant grade between vertical curves + double fixed_tolerance{0.005}; // m, allowed departure from fixed points +}; + +struct line +{ + std::vector points; // ordered by chainage; the first and the last are the ends of the route + double speed{100.0}; + limits norms; + // at the joined ends: keep the elevation and take over the grade of the adjoining track + bool join_start{true}; + bool join_end{true}; +}; + +double elevation(line const &Line, double const Chainage); +// dy/ds +double grade(line const &Line, double const Chainage); +// grade of the element between points Index and Index + 1 +double grade_after(line const &Line, std::size_t const Index); +double required_radius(line const &Line); +// radii of the automatic points: the required one where the change of grade needs a curve, none elsewhere +void assign_radii(line &Line); + +struct curve +{ + std::size_t point{0}; + double start{0.0}; + double end{0.0}; + double tangent{0.0}; + double sagitta{0.0}; + double change{0.0}; // grade after - grade before +}; +std::vector curves(line const &Line); + +// radius which gives specified elevation of the curve at the point of intersection +double radius_for_sagitta(line const &Line, std::size_t const Point, double const Sagitta); + +// facts about the route which the checks take into account +struct context +{ + double length{0.0}; + struct zone + { + double from{0.0}; + double to{0.0}; + std::string name; + }; + std::vector switches; + // elevations which the grade line has to keep (chainage, elevation) + std::vector> fixed; + // adjoining track at the ends; grades rise along the route + double start_elevation{0.0}, end_elevation{0.0}; + double start_grade{0.0}, end_grade{0.0}; + bool start_joined{false}, end_joined{false}; +}; + +// locks the joined ends, places the joint points and sets the automatic radii +void complete(line &Line, context const &Context); + +struct issue +{ + bool error{false}; + int point{-1}; + double chainage{0.0}; + std::string text; +}; +std::vector check(line const &Line, context const &Context); + +struct recognition +{ + double grade_tolerance{0.0004}; + double minimum_length{15.0}; + double sharp_gap{2.0}; +}; +// grade line of existing track, from its elevations and grades sampled along the chainage +line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options); + +// chainages where the paths have to end so that each of them carries a single grade or a single curve +std::vector breaks(line const &Line); + +std::string format_chainage(double const Chainage); + +} diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 9c2bb100..562f6391 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -9,6 +9,8 @@ http: #include "stdafx.h" #include "editor/editorTrack.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" #include "world/Track.h" #include "vehicle/DynObj.h" @@ -27,12 +29,20 @@ http: #include #include #include +#include #include namespace { -double const kSamePoint{0.05}; +using geometry::bezier; +using geometry::chord_length; +using geometry::cross; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; + +double const kSamePoint{geometry::same_point}; segment_data::point point_index(editor_track::point_kind const Kind) { @@ -45,11 +55,6 @@ segment_data::point point_index(editor_track::point_kind const Kind) } } -bool is_end(editor_track::point_kind const Kind) -{ - return Kind == editor_track::point_kind::start || Kind == editor_track::point_kind::end; -} - bool has_switch_paths(TTrack const &Track) { return Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross); @@ -74,17 +79,11 @@ glm::dvec3 path_tangent(segment_data const &Path, bool const Atend) double path_radius(segment_data const &Path, double const T) { - auto const &p0{Path.points[segment_data::point::start]}; - auto const &p3{Path.points[segment_data::point::end]}; - auto const &control1{Path.points[segment_data::point::control1]}; - auto const &control2{Path.points[segment_data::point::control2]}; - if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + if (Path.points[segment_data::point::control1] == glm::dvec3{} && Path.points[segment_data::point::control2] == glm::dvec3{}) return 0.0; - auto const p1{p0 + control1}; - auto const p2{p3 + control2}; - auto const u{1.0 - T}; - auto const first{3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2)}; - auto const second{6.0 * u * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1)}; + bezier const curve{Path}; + auto const first{curve.first(T)}; + auto const second{curve.second(T)}; auto const speed{std::hypot(first.x, first.z)}; auto const curvature{std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed)}; return curvature > 1e-7 ? 1.0 / curvature : 0.0; @@ -123,8 +122,12 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool } +editor_track::observer *editor_track::s_observer{nullptr}; + editor_track::state editor_track::capture(TTrack const &Track) { + if (s_observer != nullptr) + s_observer->track_captured(Track); state result; result.paths = Track.m_paths; result.velocity = Track.fVelocity; @@ -237,6 +240,8 @@ void editor_track::commit(std::vector const &Tracks) for (auto *track : affected) for (auto *vehicle : track->Dynamics) vehicle->Move(0.000001); + if (s_observer != nullptr) + s_observer->tracks_committed(Tracks); } void editor_track::commit_parameters(TTrack &Track) @@ -384,7 +389,7 @@ std::vector> editor_track::connecte bool editor_track::is_connected(TTrack const &Track, point_ref const &Point) { - if (false == is_end(Point.kind)) + if (false == editor_track::is_end(Point.kind)) return false; return false == connected_points(Track, point_position(Track, Point)).empty(); } @@ -447,7 +452,7 @@ editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, gl void editor_track::snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent) { - if (Target.track == nullptr || false == is_end(Point.kind)) + if (Target.track == nullptr || false == editor_track::is_end(Point.kind)) return; move_point(Track, Point, Target.position); @@ -777,23 +782,6 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin namespace { -glm::dvec2 plan_of(glm::dvec3 const &Point) -{ - return {Point.x, Point.z}; -} - -glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle) -{ - auto const c{std::cos(Angle)}; - auto const s{std::sin(Angle)}; - return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; -} - -double lateral_of(glm::dvec2 const &Direction, glm::dvec2 const &Offset) -{ - return Direction.x * Offset.y - Direction.y * Offset.x; -} - bool plan_straight(segment_data const &Path, editor_track::straight_tolerance const &Tolerance) { auto const chord{plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; @@ -808,7 +796,7 @@ bool plan_straight(segment_data const &Path, editor_track::straight_tolerance co return true; auto const direction{chord / length}; for (auto const index : {segment_data::point::control1, segment_data::point::control2}) - if (std::abs(lateral_of(direction, plan_of(Path.points[index]))) > Tolerance.offset) + if (std::abs(cross(direction, plan_of(Path.points[index]))) > Tolerance.offset) return false; auto const tangentstart{plan_of(path_tangent(Path, false))}; auto const tangentend{plan_of(path_tangent(Path, true))}; @@ -816,8 +804,8 @@ bool plan_straight(segment_data const &Path, editor_track::straight_tolerance co return false; auto const a{glm::normalize(tangentstart)}; auto const b{glm::normalize(tangentend)}; - auto const turn{std::abs(std::atan2(lateral_of(a, b), glm::dot(a, b)))}; - auto const deviation{std::max(std::abs(lateral_of(direction, a)), std::abs(lateral_of(direction, b)))}; + auto const turn{std::abs(std::atan2(cross(a, b), glm::dot(a, b)))}; + auto const deviation{std::max(std::abs(cross(direction, a)), std::abs(cross(direction, b)))}; return turn * Tolerance.radius <= length && deviation * Tolerance.radius <= length; } @@ -841,11 +829,6 @@ glm::dvec2 direction_at(TTrack const &Track, glm::dvec3 const &Joint, bool const return {0.0, 1.0}; } -double signed_angle(glm::dvec2 const &From, glm::dvec2 const &To) -{ - return std::atan2(lateral_of(From, To), glm::dot(From, To)); -} - TTrack *neighbour_at(TTrack const &Track, int const Path, bool const Atend) { if (has_switch_paths(Track)) @@ -928,7 +911,7 @@ editor_track::straight editor_track::find_straight(TTrack &Track, straight_toler auto const &first{Track.m_paths[firstpath]}; auto const origin{first.points[segment_data::point::start]}; auto const direction{glm::normalize(plan_of(first.points[segment_data::point::end] - origin))}; - auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(lateral_of(direction, plan_of(Point - origin))); }; + auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(cross(direction, plan_of(Point - origin))); }; auto const accepts = [&](TTrack const *Other, glm::dvec3 const &Joint) { if (Other == nullptr || false == is_supported(*Other) || Other->m_editorremoved) return -1; @@ -942,7 +925,7 @@ editor_track::straight editor_track::find_straight(TTrack &Track, straight_toler if (glm::distance(start, Joint) > kSamePoint && glm::distance(end, Joint) > kSamePoint) continue; auto const otherdirection{glm::normalize(plan_of(end - start))}; - if (std::abs(lateral_of(direction, otherdirection)) > Tolerance.angle) + if (std::abs(cross(direction, otherdirection)) > Tolerance.angle) continue; if (lateral(start) > Tolerance.offset || lateral(end) > Tolerance.offset) continue; @@ -1182,6 +1165,538 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance return true; } +namespace +{ + +// shorter remainders of a straight path are absorbed by moving the joint instead of splitting the path +double split_margin(double const Length) +{ + return std::max(0.5, 0.002 * Length); +} + +} // namespace + +editor_track::straight_run editor_track::straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance) +{ + straight_run run; + if (Chain.tracks.empty()) + return run; + auto *edge{Atend ? Chain.tracks.back() : Chain.tracks.front()}; + bool const forward{Atend ? Chain.forward.back() : Chain.forward.front()}; + TTrack *current{forward == Atend ? edge->trNext : edge->trPrev}; + auto joint{Atend ? Chain.end : Chain.start}; + auto const origin{plan_of(joint)}; + auto direction{glm::normalize(plan_of(Atend ? Chain.end_direction : -Chain.start_direction))}; + while (current != nullptr && run.tracks.size() < 1000) + { + std::string reason; + if (current->eType != tt_Normal || current->m_editorremoved || false == can_edit_geometry(*current, reason)) + break; + if (std::find(Chain.tracks.begin(), Chain.tracks.end(), current) != Chain.tracks.end() || std::find(run.tracks.begin(), run.tracks.end(), current) != run.tracks.end()) + break; + auto const &path{current->m_paths.front()}; + if (false == plan_straight(path, Tolerance)) + break; + bool outward; + if (glm::distance(path.points[segment_data::point::start], joint) <= kSamePoint) + outward = true; + else if (glm::distance(path.points[segment_data::point::end], joint) <= kSamePoint) + outward = false; + else + break; + auto const &opposite{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + if (run.tracks.empty()) + { + auto const own{glm::normalize(plan_of(opposite - joint))}; + if (std::abs(cross(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0) + break; + direction = own; + } + auto const offset{plan_of(opposite) - origin}; + auto const along{glm::dot(offset, direction)}; + if (along <= run.length + 1e-3 || std::abs(cross(direction, offset)) > Tolerance.offset) + break; + run.tracks.push_back(current); + run.outward.push_back(outward); + run.length = along; + joint = opposite; + current = outward ? current->trNext : current->trPrev; + } + return run; +} + +double editor_track::run_reserve(straight_run const &Run) +{ + if (Run.tracks.empty()) + return 0.0; + return std::max(0.0, Run.length - split_margin(chord_length(Run.tracks.back()->m_paths.front())) - 0.01); +} + +TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created) +{ + auto const length{glm::length(plan_of(Cut - Joint))}; + auto *edge{Edge}; + double covered{0.0}; + for (std::size_t i = 0; i < Run.tracks.size(); ++i) + { + auto *track{Run.tracks[i]}; + auto const outward{Run.outward[i]}; + auto const &path{track->m_paths.front()}; + auto const pathlength{chord_length(path)}; + auto const margin{split_margin(pathlength)}; + auto const remaining{length - covered}; + if (remaining <= margin) + { + if (std::abs(remaining) > 1e-4) + { + States.emplace_back(track, capture(*track)); + move_point(*track, {0, outward ? point_kind::start : point_kind::end}, Cut); + commit({track}); + } + break; + } + if (remaining < pathlength - margin) + { + auto const &nearpoint{path.points[outward ? segment_data::point::start : segment_data::point::end]}; + auto const &farpoint{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + auto const t{nearest_parameter(*track, nearpoint + (farpoint - nearpoint) * (remaining / pathlength))}; + auto const before{capture(*track)}; + auto *created{split_path(*track, t)}; + if (created == nullptr) + break; + States.emplace_back(track, before); + Created.push_back(created); + edge = outward ? track : created; + break; + } + edge = track; + covered += pathlength; + } + return edge; +} + +namespace +{ + +using route_node = std::tuple; + +std::vector route_next(route_node const &Node) +{ + std::vector result; + auto *track{std::get<0>(Node)}; + auto const path{std::get<1>(Node)}; + auto const forward{std::get<2>(Node)}; + auto const &exit{track->m_paths[path].points[forward ? segment_data::point::end : segment_data::point::start]}; + auto *next{neighbour_at(*track, path, forward)}; + if (next == nullptr || next == track || next->m_editorremoved || false == editor_track::is_supported(*next) || (next->iCategoryFlag & 15) != (track->iCategoryFlag & 15)) + return result; + for (int i = 0; i < static_cast(next->m_paths.size()); ++i) + { + auto const &candidate{next->m_paths[i]}; + if (glm::distance(candidate.points[segment_data::point::start], exit) <= kSamePoint) + result.emplace_back(next, i, true); + else if (glm::distance(candidate.points[segment_data::point::end], exit) <= kSamePoint) + result.emplace_back(next, i, false); + } + return result; +} + +} // namespace + +bool editor_track::find_route(TTrack *From, TTrack *To, route &Route, std::string &Error) +{ + Route = {}; + if (From == nullptr || To == nullptr || false == is_supported(*From) || false == is_supported(*To)) + { + Error = "Select the first and the last path of the route"; + return false; + } + if (From == To) + { + Route.spans.push_back({From, 0, true}); + Error.clear(); + return true; + } + std::vector nodes; + std::vector parents; + std::map visited; + for (int i = 0; i < static_cast(From->m_paths.size()); ++i) + for (auto const forward : {true, false}) + { + route_node const node{From, i, forward}; + visited.emplace(node, static_cast(nodes.size())); + nodes.push_back(node); + parents.push_back(-1); + } + int goal{-1}; + for (std::size_t index = 0; index < nodes.size() && goal < 0 && nodes.size() < 200000; ++index) + { + for (auto const &next : route_next(nodes[index])) + { + if (std::get<0>(next) == From || visited.count(next) > 0) + continue; + visited.emplace(next, static_cast(nodes.size())); + nodes.push_back(next); + parents.push_back(static_cast(index)); + if (std::get<0>(next) == To) + { + goal = static_cast(nodes.size()) - 1; + break; + } + } + } + if (goal < 0) + { + Error = "No route connects the selected paths"; + return false; + } + for (int index = goal; index >= 0; index = parents[index]) + Route.spans.push_back({std::get<0>(nodes[index]), std::get<1>(nodes[index]), std::get<2>(nodes[index])}); + std::reverse(Route.spans.begin(), Route.spans.end()); + Error.clear(); + return true; +} + +editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) +{ + route result; + if (false == is_supported(Track)) + return result; + auto const walk = [&](route_node Node) { + std::vector nodes; + double length{0.0}; + while (nodes.size() < 5000 && length < Maximum) + { + auto candidates{route_next(Node)}; + if (candidates.empty()) + break; + auto chosen{candidates.front()}; + for (auto const &candidate : candidates) + if (std::get<1>(candidate) == 0) + chosen = candidate; + auto *track{std::get<0>(chosen)}; + if (track == &Track || std::any_of(nodes.begin(), nodes.end(), [&](route_node const &Other) { return std::get<0>(Other) == track; })) + break; + nodes.push_back(chosen); + length += bezier(track->m_paths[std::get<1>(chosen)]).plan_length(8); + Node = chosen; + } + return nodes; + }; + auto const ahead{walk({&Track, 0, true})}; + auto const behind{walk({&Track, 0, false})}; + for (auto node = behind.rbegin(); node != behind.rend(); ++node) + if (std::none_of(ahead.begin(), ahead.end(), [&](route_node const &Other) { return std::get<0>(Other) == std::get<0>(*node); })) + result.spans.push_back({std::get<0>(*node), std::get<1>(*node), false == std::get<2>(*node)}); + result.spans.push_back({&Track, 0, true}); + for (auto const &node : ahead) + result.spans.push_back({std::get<0>(node), std::get<1>(node), std::get<2>(node)}); + return result; +} + +editor_track::route editor_track::route_of(chain const &Chain) +{ + route result; + for (std::size_t i = 0; i < Chain.tracks.size(); ++i) + result.spans.push_back({Chain.tracks[i], 0, Chain.forward[i]}); + return result; +} + +std::vector editor_track::sample_route(route &Route, double const Step) +{ + std::vector samples; + double chainage{0.0}; + for (std::size_t i = 0; i < Route.spans.size(); ++i) + { + auto &span{Route.spans[i]}; + auto const &path{span.track->m_paths[span.path]}; + bezier const curve{path}; + auto const count{std::max(2, static_cast(std::ceil(curve.plan_length(16) / std::max(0.1, Step))))}; + span.from = chainage; + auto previous{plan_of(curve.point(span.forward ? 0.0 : 1.0))}; + for (int j = (samples.empty() ? 0 : 1); j <= count; ++j) + { + auto const t{span.forward ? static_cast(j) / count : 1.0 - static_cast(j) / count}; + auto const position{curve.point(t)}; + chainage += glm::distance(previous, plan_of(position)); + previous = plan_of(position); + auto derivative{curve.first(t)}; + if (false == span.forward) + derivative = -derivative; + auto const second{curve.second(t)}; + auto const planar{std::hypot(derivative.x, derivative.z)}; + route_sample sample; + sample.chainage = chainage; + sample.position = position; + sample.span = i; + sample.cant = 1.5 * std::sin(std::abs(glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t))); + if (planar > 1e-9) + { + sample.direction = plan_of(derivative) / planar; + sample.grade = derivative.y / planar; + sample.curvature = (derivative.x * second.z - derivative.z * second.x) / (planar * planar * planar); + } + samples.push_back(sample); + } + span.to = chainage; + } + Route.length = chainage; + return samples; +} + +namespace +{ + +template +auto interpolate(std::vector const &Samples, double const Chainage, Getter const &Get) -> decltype(Get(Samples.front())) +{ + if (Samples.empty()) + return {}; + auto const next{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + if (next == Samples.begin()) + return Get(Samples.front()); + if (next == Samples.end()) + return Get(Samples.back()); + auto const &before{*std::prev(next)}; + auto const run{next->chainage - before.chainage}; + return run > 1e-9 ? Get(before) + (Get(*next) - Get(before)) * (Chainage - before.chainage) / run : Get(before); +} + +void set_heights(segment_data &Path, double const Startheight, double const Endheight, double const Startgrade, double const Endgrade) +{ + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + start.y = Startheight; + end.y = Endheight; + control1.y = Startgrade * std::hypot(control1.x, control1.z); + control2.y = -Endgrade * std::hypot(control2.x, control2.z); +} + +// the path lies in the plane through Origin which rises with Grade along Direction (unit, in plan) and is level across it +void plane_heights(segment_data &Path, glm::dvec3 const &Origin, glm::dvec2 const &Direction, double const Grade) +{ + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + auto const height = [&](glm::dvec3 const &Point) { return Origin.y + Grade * glm::dot(plan_of(Point - Origin), Direction); }; + auto const first{start + control1}; + auto const second{end + control2}; + start.y = height(start); + end.y = height(end); + control1.y = height(first) - start.y; + control2.y = height(second) - end.y; +} + +glm::dvec2 start_direction(segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control{Path.points[segment_data::point::control1]}; + glm::dvec2 direction{control.x, control.z}; + if (glm::length(direction) < 1e-6) + direction = {Path.points[segment_data::point::end].x - start.x, Path.points[segment_data::point::end].z - start.z}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0}; +} + +} // namespace + +double editor_track::sampled_elevation(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position.y; }); +} + +double editor_track::sampled_grade(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.grade; }); +} + +glm::dvec3 editor_track::sampled_position(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position; }); +} + +bool editor_track::adjoining_grade(route const &Route, bool const Atend, double &Grade) +{ + if (Route.spans.empty()) + return false; + auto const &span{Atend ? Route.spans.back() : Route.spans.front()}; + auto const outward{Atend ? span.forward : false == span.forward}; + auto const next{route_next({span.track, span.path, outward})}; + if (next.empty()) + return false; + auto const &[track, path, forward]{next.front()}; + auto const tangent{path_tangent(track->m_paths[path], false == forward)}; + auto const plan{std::hypot(tangent.x, tangent.z)}; + if (plan < 1e-9) + return false; + auto const along{(forward ? 1.0 : -1.0) * tangent.y / plan}; + Grade = Atend ? along : -along; + return true; +} + +std::vector editor_track::apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created) +{ + double constexpr margin{0.25}; + auto const remember = [&](TTrack *Track) { + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; }) && std::find(Created.begin(), Created.end(), Track) == Created.end()) + States.emplace_back(Track, capture(*Track)); + }; + sample_route(Route, 1.0); + std::vector spans; + for (auto const &span : Route.spans) + { + if (span.track->eType != tt_Normal) + { + spans.push_back(span); + continue; + } + std::vector cuts; + for (auto const chainage : Breaks) + if (chainage > span.from + margin && chainage < span.to - margin) + cuts.push_back(chainage); + if (cuts.empty()) + { + spans.push_back(span); + continue; + } + remember(span.track); + if (span.forward) + { + auto *piece{span.track}; + auto origin{span.from}; + spans.push_back({piece, 0, true}); + for (auto const cut : cuts) + { + auto const t{bezier(piece->m_paths.front()).parameter(cut - origin)}; + auto *created{split_path(*piece, t)}; + if (created == nullptr) + continue; + Created.push_back(created); + spans.push_back({created, 0, true}); + piece = created; + origin = cut; + } + } + else + { + std::vector order; + for (auto cut = cuts.begin(); cut != cuts.end(); ++cut) + { + auto const t{bezier(span.track->m_paths.front()).parameter(span.to - *cut)}; + auto *created{split_path(*span.track, t)}; + if (created == nullptr) + continue; + Created.push_back(created); + order.push_back(created); + } + for (auto *piece : order) + spans.push_back({piece, 0, false}); + spans.push_back({span.track, 0, false}); + } + } + Route.spans = spans; + sample_route(Route, 1.0); + + auto const on_route = [&](TTrack const *Track) { + return std::any_of(Route.spans.begin(), Route.spans.end(), [&](route_span const &Span) { return Span.track == Track; }); + }; + struct joint + { + TTrack *track; + int path; + bool atend; + TTrack *neighbour; + }; + std::vector joints; + std::vector changed; + for (auto const &span : Route.spans) + { + remember(span.track); + auto const startchainage{span.forward ? span.from : span.to}; + auto const endchainage{span.forward ? span.to : span.from}; + auto const sign{span.forward ? 1.0 : -1.0}; + auto &path{span.track->m_paths[span.path]}; + set_heights(path, Elevation(startchainage), Elevation(endchainage), Grade(startchainage) * sign, Grade(endchainage) * sign); + add_unique(changed, span.track); + if (span.track->eType == tt_Normal) + continue; + auto const grade{Grade((span.from + span.to) * 0.5) * sign}; + for (int i = 0; i < static_cast(span.track->m_paths.size()); ++i) + { + for (auto const atend : {false, true}) + { + auto *neighbour{neighbour_at(*span.track, i, atend)}; + if (neighbour != nullptr && false == on_route(neighbour) && std::none_of(joints.begin(), joints.end(), [&](joint const &Joint) { return Joint.track == span.track && Joint.neighbour == neighbour; })) + joints.push_back({span.track, i, atend, neighbour}); + } + if (i != span.path) + plane_heights(span.track->m_paths[i], path.points[segment_data::point::start], start_direction(path), grade); + } + } + commit(changed); + sample_route(Route, 1.0); + + std::vector gaps; + for (auto const &joint : joints) + { + auto const &end{joint.track->m_paths[joint.path].points[joint.atend ? segment_data::point::end : segment_data::point::start]}; + double nearest{std::numeric_limits::max()}; + double gap{0.0}; + for (auto const &path : joint.neighbour->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &point{path.points[index]}; + auto const distance{std::hypot(point.x - end.x, point.z - end.z)}; + if (distance < nearest) + { + nearest = distance; + gap = point.y - end.y; + } + } + if (nearest <= kSamePoint && std::abs(gap) > 0.001) + gaps.push_back({joint.track, joint.neighbour, gap}); + } + return gaps; +} + +void editor_track::keep_heights(chain const &Chain, std::vector &Pieces) +{ + auto existing{route_of(Chain)}; + auto const samples{sample_route(existing, 1.0)}; + if (samples.empty() || existing.length <= 1e-6) + return; + std::vector lengths; + double total{0.0}; + for (auto const &piece : Pieces) + { + lengths.push_back(bezier(piece).plan_length()); + total += lengths.back(); + } + if (total <= 1e-6) + return; + auto const scale{existing.length / total}; + double chainage{0.0}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + auto const from{chainage * scale}; + auto const to{(chainage + lengths[i]) * scale}; + set_heights(Pieces[i], sampled_elevation(samples, from), sampled_elevation(samples, to), sampled_grade(samples, from) * scale, sampled_grade(samples, to) * scale); + chainage += lengths[i]; + } +} + std::vector editor_track::find_switch_templates() { std::vector result; @@ -1213,7 +1728,7 @@ std::vector editor_track::find_switch_templates() local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]); local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]); } - if (glm::length(glm::dvec2{entry.local[1].points[segment_data::point::start].x, entry.local[1].points[segment_data::point::start].z}) > kSamePoint) + if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint) continue; if (entry.local[1].points[segment_data::point::end].x < 0.0) { @@ -1229,8 +1744,7 @@ std::vector editor_track::find_switch_templates() auto const angle{std::atan2(std::abs(tangent.x), tangent.z)}; entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0; entry.radius = path_radius(entry.local[1], 0.25); - char key[256]; - std::snprintf(key, sizeof(key), "%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str()); + auto const key{format("%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str())}; auto const found{lookup.find(key)}; if (found != lookup.end()) { @@ -1238,9 +1752,7 @@ std::vector editor_track::find_switch_templates() continue; } entry.count = 1; - char label[256]; - std::snprintf(label, sizeof(label), "L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str()); - entry.label = label; + entry.label = format("L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str()); lookup.emplace(key, result.size()); result.push_back(entry); } @@ -1324,6 +1836,44 @@ std::vector editor_track::standard_switch_templat return result; } +namespace +{ + +// definition of a path in the scenery text, up to its points: type, length, parameters, environment and textures +void write_header(std::ostream &Text, char const *Type, double const Length, double const Sound, TTrack const &Style, material_handle const Second) +{ + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"}; + Text << Type << ' ' << Length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Sound << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' '; + if (Style.m_visible) + Text << "vis " << editor_track::material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << editor_track::material_name(Second) << ' ' << editor_track::texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' '; + else + Text << "unvis "; +} + +void write_points(std::ostream &Text, segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + auto const &end{Path.points[segment_data::point::end]}; + Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; +} + +void write_ending(std::ostream &Text, TTrack const &Style) +{ + if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) + Text << "railprofile " << Style.m_profile1.first << ' '; + Text << "endtrack\n"; +} + +material_handle ballast_of(TTrack const &Style) +{ + return Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2; +} + +} // namespace + TTrack *editor_track::create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name) { if (Paths.size() < 2) @@ -1335,31 +1885,14 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector } else { - char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; - auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"}; std::ostringstream text; text.precision(std::numeric_limits::digits10); - text << "switch " << Template.length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << 10.0 << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' '; - auto const rails{Style.m_material1}; - auto const trackbed{Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2}; - if (Style.m_visible) - text << "vis " << material_name(rails) << ' ' << Style.fTexLength << ' ' << material_name(rails) << ' ' << texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' '; - else - text << "unvis "; - for (int i = 0; i < 2; ++i) - { - auto const &path{Paths[i]}; - auto const &start{path.points[segment_data::point::start]}; - auto const &control1{path.points[segment_data::point::control1]}; - auto const &control2{path.points[segment_data::point::control2]}; - auto const &end{path.points[segment_data::point::end]}; - text << start.x << ' ' << start.y << ' ' << start.z << ' ' << path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << path.rolls[1] << ' ' << path.radius << ' '; - } - if (trackbed != null_handle) + write_header(text, "switch", Template.length, 10.0, Style, Style.m_material1); + write_points(text, Paths[0]); + write_points(text, Paths[1]); + if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle) text << "trackbed " << material_name(trackbed) << ' '; - if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) - text << "railprofile " << Style.m_profile1.first << ' '; - text << "endtrack\n"; + write_ending(text, Style); track = load_path(text.str(), Style, Name); } track->m_paths.assign(Paths.begin(), Paths.begin() + 2); @@ -1443,7 +1976,7 @@ void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start, if (newlength < 1e-3) return; auto const newdirection{newplan / newlength}; - auto const angle{std::atan2(lateral_of(direction, newdirection), glm::dot(direction, newdirection))}; + auto const angle{std::atan2(cross(direction, newdirection), glm::dot(direction, newdirection))}; auto const along = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - Line.start), direction); }; struct interval @@ -1483,7 +2016,7 @@ void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start, }; auto const transform = [&](glm::dvec3 const &Point) { auto const a{along(Point)}; - auto const offset{lateral_of(direction, plan_of(Point - Line.start))}; + auto const offset{cross(direction, plan_of(Point - Line.start))}; auto const mapped{remap(a)}; auto const oldheight{Line.start.y + (Line.end.y - Line.start.y) * a / Line.length}; auto const newheight{Start.y + (End.y - Start.y) * mapped / newlength}; @@ -1613,47 +2146,35 @@ TTrack *editor_track::clone(TTrack const &Template) TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) { - char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; - auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"}; auto const category{Style.iCategoryFlag & 15}; std::ostringstream text; text.precision(std::numeric_limits::digits10); - text << (category == 2 ? "road " : category == 4 ? "river " : "normal ") << glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]) << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Style.fSoundDistance << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' '; - auto const ballast{Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2}; - if (Style.m_visible) - text << "vis " << material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << material_name(ballast) << ' ' << texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' '; - else - text << "unvis "; - auto const &start{Path.points[segment_data::point::start]}; - auto const &control1{Path.points[segment_data::point::control1]}; - auto const &control2{Path.points[segment_data::point::control2]}; - auto const &end{Path.points[segment_data::point::end]}; - text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; - auto const speed{velocity(Style)}; - if (speed > 0.0) + write_header(text, category == 2 ? "road" : category == 4 ? "river" : "normal", glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]), Style.fSoundDistance, Style, ballast_of(Style)); + write_points(text, Path); + if (auto const speed{velocity(Style)}; speed > 0.0) text << "velocity " << speed << ' '; - if (category == 1 && false == Style.m_profile1.first.empty()) - text << "railprofile " << Style.m_profile1.first << ' '; - text << "endtrack\n"; + write_ending(text, Style); auto *track{load_path(text.str(), Style)}; track->m_paths = {Path}; return track; } +glm::dvec3 editor_track::point_at(TTrack const &Track, double const T) +{ + auto const &path{Track.m_paths.front()}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; +} + double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Point) { if (Track.m_paths.empty()) return 0.5; - auto const &path{Track.m_paths.front()}; - auto const p0{path.points[segment_data::point::start]}; - auto const p3{path.points[segment_data::point::end]}; - auto const p1{p0 + path.points[segment_data::point::control1]}; - auto const p2{p3 + path.points[segment_data::point::control2]}; - auto const at = [&](double const T) { - auto const u{1.0 - T}; - return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; - }; - auto const distance = [&](double const T) { auto const q{at(T)}; return glm::distance(glm::dvec2{q.x, q.z}, glm::dvec2{Point.x, Point.z}); }; + auto const distance = [&](double const T) { return geometry::plan_distance(point_at(Track, T), Point); }; double best{0.5}; for (int i = 0; i <= 200; ++i) if (distance(i / 200.0) < distance(best)) @@ -1707,6 +2228,8 @@ TTrack *editor_track::split_path(TTrack &Track, double const T) second.rolls[0] = roll; auto *created{create_path(Track, second)}; Track.m_paths.front() = first; + if (s_observer != nullptr) + s_observer->track_split(Track, *created, first); commit({&Track, created}); return created; } @@ -1743,12 +2266,8 @@ void editor_track::retire(TTrack &Track) update_transition(*neighbour); rebuild_geometry(*neighbour); } -} - -void editor_track::revive(TTrack &Track) -{ - Track.m_editorremoved = false; - commit({&Track}); + if (s_observer != nullptr) + s_observer->track_retired(Track); } void editor_track::rebuild_geometry(TTrack &Track) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 45035212..d45ecf54 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -25,6 +25,7 @@ class editor_track { public: enum class point_kind { start, control1, control2, end }; + static bool is_end(point_kind const Kind) { return Kind == point_kind::start || Kind == point_kind::end; } struct point_ref { int path{-1}; @@ -108,6 +109,8 @@ class editor_track double length{0.0}; double grade{0.0}; double azimuth{0.0}; + // distance of the point from the start, measured along the line in the plan + double along(glm::dvec3 const &Point) const { return (Point.x - start.x) * direction.x + (Point.z - start.z) * direction.y; } }; struct straight_tolerance { @@ -137,6 +140,63 @@ class editor_track double split{0.5}; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); + // regular straight paths continuing a chain end in line, which the chain can take over + struct straight_run + { + std::vector tracks; + std::vector outward; + double length{0.0}; + }; + static straight_run straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance); + static double run_reserve(straight_run const &Run); + static TTrack *take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created); + // sequence of paths, switches included, which a train can run along + struct route_span + { + TTrack *track{nullptr}; + int path{0}; + bool forward{true}; + double from{0.0}; // chainage, measured in the plan + double to{0.0}; + }; + struct route + { + std::vector spans; + double length{0.0}; + }; + static bool find_route(TTrack *From, TTrack *To, route &Route, std::string &Error); + // continues both ways from the path, through switches along their path 0 + static route run_route(TTrack &Track, double const Maximum); + static route route_of(chain const &Chain); + struct route_sample + { + double chainage{0.0}; + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + double curvature{0.0}; // signed, 1/m, positive turning left + double cant{0.0}; // m, raise of the outer rail + std::size_t span{0}; + }; + static std::vector sample_route(route &Route, double const Step); + static double sampled_elevation(std::vector const &Samples, double const Chainage); + static double sampled_grade(std::vector const &Samples, double const Chainage); + static glm::dvec3 sampled_position(std::vector const &Samples, double const Chainage); + // grade of the path which adjoins the end of the route, positive rising along the route + static bool adjoining_grade(route const &Route, bool const Atend, double &Grade); + // path off the route whose end no longer meets the adjoining path in height + struct height_gap + { + TTrack *track{nullptr}; + TTrack *neighbour{nullptr}; + double gap{0.0}; // neighbour - track + }; + // sets elevations of the paths of the route; regular paths are split at the chainages in Breaks, + // switches are tilted as a whole in the plane of their grade, branches included + static std::vector apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created); + // carries elevations of the chain over to the new pieces, in proportion of the length + static void keep_heights(chain const &Chain, std::vector &Pieces); + struct switch_template { TTrack *source{nullptr}; @@ -156,12 +216,13 @@ class editor_track static TTrack *create_path(TTrack const &Style, segment_data const &Path); static TTrack *split_path(TTrack &Track, double const T); static double nearest_parameter(TTrack const &Track, glm::dvec3 const &Point); + // point of the first path at the parameter of nearest_parameter() + static glm::dvec3 point_at(TTrack const &Track, double const T); static std::vector place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade); static TTrack *create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name = {}); static void move_straights(std::vector const &Lines, std::vector> const &Ends); static std::vector relay(chain const &Chain, std::vector const &Pieces); static void retire(TTrack &Track); - static void revive(TTrack &Track); static std::string material_name(material_handle const Material); static material_handle fetch_material(std::string const &Name); @@ -172,7 +233,21 @@ class editor_track static void velocity(TTrack &Track, double const Velocity); static void damage(TTrack &Track, int const Damage); + // told about the changes of the paths, to let the objects which belong to them follow + class observer + { + public: + virtual ~observer() = default; + virtual void track_captured(TTrack const &Track) = 0; + virtual void tracks_committed(std::vector const &Tracks) = 0; + virtual void track_retired(TTrack &Track) = 0; + // called before the commit; Original keeps the beginning of the path, given in First + virtual void track_split(TTrack &Original, TTrack &Created, segment_data const &First) = 0; + }; + static void observe(observer *Observer) { s_observer = Observer; } + private: + static observer *s_observer; static void disconnect(TTrack &Track); static void join(TTrack &Track); static void connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid); diff --git a/rendering/editoroverlay.h b/rendering/editoroverlay.h new file mode 100644 index 00000000..57442d43 --- /dev/null +++ b/rendering/editoroverlay.h @@ -0,0 +1,41 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +#include + +namespace gfx +{ + +// flat coloured geometry the editor shows in the 3d view, drawn with the depth test over the translucent scenery +struct editor_overlay +{ + struct batch + { + glm::vec4 color{1.0f}; + unsigned int type{0}; // GL primitive + std::vector points; // relative to the origin + }; + glm::dvec3 origin{0.0}; + std::vector batches; + unsigned int revision{0}; // changed with every new content + + void clear() + { + batches.clear(); + ++revision; + } +}; + +} // namespace gfx + +inline gfx::editor_overlay EditorOverlay; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 767618a1..b146d5d1 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "rendering/opengl33geometrybank.h" #include "rendering/screenshot.h" +#include "rendering/editoroverlay.h" #include //#define EU07_DEBUG_OPENGL @@ -933,6 +934,7 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode) glDebug("render translucent region"); setup_drawing(true); Render_Alpha(simulation::Region); + Render_editor_overlay(); // particles Render_particles(); @@ -1518,6 +1520,17 @@ glm::mat4 opengl33_renderer::ortho_frustumtest_projection(float l, float r, floa return glm::ortho(l, r, b, t, znear, zfar); } +// the main viewport of the editor in the orthographic view replaces the perspective projection +void opengl33_renderer::editor_ortho_projection(viewport_config const &Viewport, float const Zfar, glm::mat4 &Projection, glm::mat4 &Frustum) +{ + if (false == (EditorModeFlag && Global.EditorOrtho && Viewport.main)) + return; + auto const height{Global.EditorOrthoExtent}; + auto const width{height * Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; + Projection = ortho_projection(-width, width, -height, height, -Zfar, Zfar); + Frustum = ortho_frustumtest_projection(-width, width, -height, height, -Zfar, Zfar); +} + void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config &Config, rendermode const Mode, float const Znear, float const Zfar, bool const Ignoredebug) { @@ -1593,13 +1606,7 @@ void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config auto const znear = ( Znear > 1.f ? Znear : Znear > 0.f ? Znear * zfar : 0.1f * Global.ZoomFactor); camera.projection() = perspective_projection(Viewport.projection, znear, zfar, frustumtest_proj); - if (EditorModeFlag && Global.EditorOrtho && Viewport.main) - { - auto const height{Global.EditorOrthoExtent}; - auto const width{height * Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; - camera.projection() = ortho_projection(-width, width, -height, height, -zfar, zfar); - frustumtest_proj = ortho_frustumtest_projection(-width, width, -height, height, -zfar, zfar); - } + editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); break; } case rendermode::shadows: @@ -1699,13 +1706,7 @@ void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config } camera.projection() = perspective_projection(proj, znear, zfar, frustumtest_proj); - if (EditorModeFlag && Global.EditorOrtho && Viewport.main) - { - auto const height{Global.EditorOrthoExtent}; - auto const width{height * Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; - camera.projection() = ortho_projection(-width, width, -height, height, -zfar, zfar); - frustumtest_proj = ortho_frustumtest_projection(-width, width, -height, height, -zfar, zfar); - } + editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); break; } case rendermode::reflections: @@ -4490,6 +4491,55 @@ void opengl33_renderer::Render_Alpha(TTraction *Traction) glLineWidth(1.0f); } +void opengl33_renderer::Render_editor_overlay() +{ + if (false == EditorModeFlag || EditorOverlay.batches.empty()) + return; + glDebug("Render_editor_overlay"); + + if (m_editor_overlay_revision != EditorOverlay.revision) + { + m_editor_overlay_revision = EditorOverlay.revision; + if (m_editor_overlay_bank == null_handle) + m_editor_overlay_bank = Create_Bank(); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size(); ++i) + { + gfx::vertex_array vertices; + vertices.reserve(batches[i].points.size()); + for (auto const &point : batches[i].points) + vertices.emplace_back(point, glm::vec3(0.f, 1.f, 0.f), glm::vec2()); + gfx::userdata_array userdata; + if (i >= m_editor_overlay_geometry.size()) + m_editor_overlay_geometry.emplace_back(); + auto &geometry{m_editor_overlay_geometry[i]}; + if (geometry != null_handle) + m_geometry.replace(vertices, userdata, geometry); + else if (false == vertices.empty()) + geometry = m_geometry.create_chunk(vertices, userdata, m_editor_overlay_bank, batches[i].type); + } + } + + ::glPushMatrix(); + auto const origin{EditorOverlay.origin - m_renderpass.pass_camera.position()}; + ::glTranslated(origin.x, origin.y, origin.z); + ::glDisable(GL_CULL_FACE); + ::glDepthMask(GL_FALSE); + m_line_shader->bind(); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size() && i < m_editor_overlay_geometry.size(); ++i) + { + if (batches[i].points.empty() || m_editor_overlay_geometry[i] == null_handle) + continue; + model_ubs.param[0] = batches[i].color; + draw(m_editor_overlay_geometry[i]); + ++m_renderpass.draw_stats.drawcalls; + } + ::glDepthMask(GL_TRUE); + ::glEnable(GL_CULL_FACE); + ::glPopMatrix(); +} + void opengl33_renderer::Render_Alpha(scene::lines_node const &Lines) { glDebug("Render_Alpha scene::lines_node"); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 43cd24b0..506e9211 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -306,6 +306,7 @@ class opengl33_renderer : public gfx_renderer { void Render_Alpha(TAnimModel *Instance); void Render_Alpha(TTraction *Traction); void Render_Alpha(scene::lines_node const &Lines); + void Render_editor_overlay(); bool Render_Alpha(TDynamicObject *Dynamic); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance); @@ -399,6 +400,7 @@ class opengl33_renderer : public gfx_renderer { glm::mat4 perspective_projection(const viewport_proj_config &c, float n, float f, glm::mat4 &frustum); glm::mat4 ortho_projection(float left, float right, float bottom, float top, float z_near, float z_far); glm::mat4 ortho_frustumtest_projection(float left, float right, float bottom, float top, float z_near, float z_far); + void editor_ortho_projection(viewport_config const &Viewport, float const Zfar, glm::mat4 &Projection, glm::mat4 &Frustum); std::vector> m_control_pick_requests; std::vector> m_node_pick_requests; @@ -448,6 +450,9 @@ class opengl33_renderer : public gfx_renderer { std::unordered_map> m_shaders; std::unique_ptr m_line_shader; + gfx::geometrybank_handle m_editor_overlay_bank; + std::vector m_editor_overlay_geometry; + unsigned int m_editor_overlay_revision{0}; std::unique_ptr m_freespot_shader; std::unique_ptr m_billboard_shader; std::unique_ptr m_celestial_shader; diff --git a/scene/scene.cpp b/scene/scene.cpp index e72ab930..d876a7ba 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -503,6 +503,30 @@ basic_cell::erase( TMemCell *Memorycell ) { std::end( m_memorycells ) ); } +void +basic_cell::erase( TTraction *Traction ) { + + m_traction.erase( + std::remove( std::begin( m_traction ), std::end( m_traction ), Traction ), + std::end( m_traction ) ); +} + +void +basic_cell::unregister_end( TTraction *Traction ) { + + m_directories.traction.erase( + std::remove( std::begin( m_directories.traction ), std::end( m_directories.traction ), Traction ), + std::end( m_directories.traction ) ); +} + +void +basic_cell::erase( TEventLauncher *Launcher ) { + + m_eventlaunchers.erase( + std::remove( std::begin( m_eventlaunchers ), std::end( m_eventlaunchers ), Launcher ), + std::end( m_eventlaunchers ) ); +} + // registers provided path in the lookup directory of the cell void basic_cell::register_end( TTrack *Path ) { diff --git a/scene/scene.h b/scene/scene.h index 5cad1fcc..23c912d7 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -155,6 +155,14 @@ public: // removes provided memory cell from the cell void erase( TMemCell *Memorycell ); + // removes provided traction piece from the cell + void + erase( TTraction *Traction ); + void + unregister_end( TTraction *Traction ); + // removes provided event launcher from the cell + void + erase( TEventLauncher *Launcher ); // find a vehicle located nearest to specified point, within specified radius. reurns: located vehicle and distance std::tuple find( glm::dvec3 const &Point, float const Radius, bool const Onlycontrolled, bool const Findbycoupler ) const; diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 0c9a1c5d..2b8436e1 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -17,7 +17,10 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/utilities.h" +#include +#include #include +#include namespace scene { @@ -37,6 +40,17 @@ glm::dvec3 layer_context::to_local(glm::dvec3 Location) const return Location; } +glm::dvec3 layer_context::to_world(glm::dvec3 Location) const +{ + if (rotation != glm::vec3{0.f}) + { + Location = glm::rotateY(Location, glm::radians(static_cast(rotation.y))); + } + Location *= glm::dvec3{scale}; + Location += offset; + return Location; +} + bool layer_context::matches(layer_context const &Other) const { return glm::all(glm::epsilonEqual(offset, Other.offset, 1e-4)) && glm::all(glm::epsilonEqual(rotation, Other.rotation, 1e-4f)) && glm::all(glm::epsilonEqual(scale, Other.scale, 1e-5f)); @@ -62,6 +76,7 @@ layer_handle node_layers::open(std::string const &Name) m_layers.back().parent = handle(); m_layers.back().late = m_initialized; stat(layerhandle); + scan(layerhandle); } else { @@ -119,6 +134,8 @@ void node_layers::clear() m_instance = 0; m_initialized = false; m_terraindirective = false; + m_marked.clear(); + m_marking.clear(); } // indicates the scenario initialization (FirstInit) is being performed @@ -767,7 +784,104 @@ bool node_layers::merge(layer_handle Source, layer_handle Target) // true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes bool node_layers::pending() const { - return std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); }); + return false == m_marking.empty() || + std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); }); +} + +namespace +{ +// marks of the lines the editor keeps track of +std::array const editormarks{"//$p", "//$b"}; +} // namespace + +void node_layers::scan(layer_handle const Layer) +{ + m_marked.erase(Layer); + std::ifstream file{path(Layer), std::ios_base::binary}; + if (false == file.is_open()) + { + return; + } + std::string content{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (content.find("//$") == std::string::npos) + { + return; + } + std::size_t begin{0}; + while (begin < content.size()) + { + auto end{content.find('\n', begin)}; + end = (end == std::string::npos ? content.size() : end + 1); + auto const first{content.find_first_not_of(" \t", begin)}; + if (first < end) + { + for (auto const *mark : editormarks) + { + auto const length{std::char_traits::length(mark)}; + if (content.compare(first, length, mark) != 0 || (first + length < end && content[first + length] != ' ' && content[first + length] != '\t' && content[first + length] != '\r' && content[first + length] != '\n')) + { + continue; + } + auto text{content.substr(first + length, end - first - length)}; + while (false == text.empty() && (text.back() == '\n' || text.back() == '\r')) + { + text.pop_back(); + } + text.erase(0, text.find_first_not_of(" \t")); + m_marked[Layer].push_back({mark, {static_cast(begin), static_cast(end)}, std::move(text)}); + break; + } + } + begin = end; + } +} + +std::vector node_layers::marked(layer_handle const Layer, std::string const &Mark) const +{ + std::vector result; + if (auto const pending{m_marking.find({Layer, Mark})}; pending != m_marking.end()) + { + return pending->second; + } + if (auto const lookup{m_marked.find(Layer)}; lookup != m_marked.end()) + { + for (auto const &line : lookup->second) + { + if (line.mark == Mark) + { + result.push_back(line.text); + } + } + } + return result; +} + +std::vector node_layers::marked_layers(std::string const &Mark) const +{ + std::vector result; + for (auto const &entry : m_marked) + { + if (listed(entry.first) && std::any_of(entry.second.begin(), entry.second.end(), [&](marked_line const &Line) { return Line.mark == Mark; })) + { + result.push_back(entry.first); + } + } + for (auto const &entry : m_marking) + { + if (entry.first.second == Mark && std::find(result.begin(), result.end(), entry.first.first) == result.end()) + { + result.push_back(entry.first.first); + } + } + return result; +} + +void node_layers::mark(layer_handle const Layer, std::string const &Mark, std::vector Lines) +{ + if (valid(Layer)) + { + m_marking[{Layer, Mark}] = std::move(Lines); + } } // indicates start of content of a template, included by directive at specified location of the layer file being loaded diff --git a/scene/scenelayers.h b/scene/scenelayers.h index 211de91f..28517c16 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -13,6 +13,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include namespace scene @@ -44,6 +45,8 @@ struct layer_context // converts world space location to the one which produces it when loaded with this context in effect glm::dvec3 to_local(glm::dvec3 Location) const; + // reverse of to_local() + glm::dvec3 to_world(glm::dvec3 Location) const; bool matches(layer_context const &Other) const; }; @@ -261,6 +264,14 @@ class node_layers return m_terraindirective; } + // editor data kept in the scenery files as comment lines starting with a mark, e.g. "//$p ...", which the simulation skips + // text of the lines with specified mark in specified layer, with the mark taken off + std::vector marked(layer_handle Layer, std::string const &Mark) const; + // layers whose files hold lines with specified mark + std::vector marked_layers(std::string const &Mark) const; + // lines to take the place of the ones with specified mark in the layer file, on save. the mark is added to each + void mark(layer_handle Layer, std::string const &Mark, std::vector Lines); + // layer management. changes are applied to the scenery files by save() // creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason layer_handle create(std::string Name, layer_handle Parent, std::string &Reason); @@ -281,6 +292,11 @@ class node_layers { return Instance != 0 && Instance <= m_instances.size(); } + // handles of the includes run from 1 up to this number + std::size_t instance_count() const + { + return m_instances.size(); + } // grants access to specified include. NOTE: the include has to be tracked include_instance const &instance(instance_handle const Instance) const { @@ -319,9 +335,17 @@ class node_layers struct file_patch; struct composition; struct save_state; + struct marked_line + { + std::string mark; + source_span span; // whole line, its end included + std::string text; + }; // methods std::string path(layer_handle const Layer) const; void stat(layer_handle const Layer); + // finds the marked lines in the layer file + void scan(layer_handle const Layer); bool compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const; // members std::vector m_layers; // layer handle is index in the vector + 1 @@ -335,6 +359,8 @@ class node_layers instance_handle m_instance{0}; // helper, include whose template is being loaded bool m_initialized{false}; // helper, the scenario initialization was performed already bool m_terraindirective{false}; + std::map> m_marked; + std::map, std::vector> m_marking; // changes awaiting save }; extern node_layers Layers; diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 2036d063..f92f6a71 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -16,6 +16,9 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulation.h" #include "model/AnimModel.h" #include "world/MemCell.h" +#include "world/Traction.h" +#include "world/EvLaunch.h" +#include "world/Event.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/utilities.h" @@ -235,6 +238,54 @@ bool patch_memcell(std::string &Text, glm::dvec3 const &Location, std::string &E return true; } +// definition layout: node traction +// [parallel ] endtraction +bool patch_traction(std::string &Text, TTraction const &Traction, glm::dvec3 const &Offset, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 30 || tokens[0].text != "node" || tokens[4].text != "traction" || tokens.back().text != "endtraction") + { + Error = "unexpected layout of traction definition"; + return false; + } + std::vector changes; + std::size_t index{12}; + for (auto const &point : {Traction.pPoint1, Traction.pPoint2, Traction.pPoint3, Traction.pPoint4}) + { + auto const local{point - Offset}; + for (auto const value : {local.x, local.y, local.z}) + { + changes.push_back({tokens[index].begin, tokens[index].end, number(value)}); + ++index; + } + } + auto const &p1{Traction.pPoint1}; + auto const &p2{Traction.pPoint2}; + auto const &p3{Traction.pPoint3}; + auto const &p4{Traction.pPoint4}; + changes.push_back({tokens[24].begin, tokens[24].end, number((p3.y - p1.y + p4.y - p2.y) * 0.5 - Traction.fHeightDifference)}); + if (Traction.iNumSections > 0) + { + // the load truncates length / segment length, the margin keeps the rounding of the number from losing a section + changes.push_back({tokens[25].begin, tokens[25].end, number(glm::length(p1 - p2) / (Traction.iNumSections + 0.001))}); + } + apply(Text, changes); + return true; +} + +// definition layout: node eventlauncher ... end +bool patch_launcher(std::string &Text, glm::dvec3 const &Location, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 9 || tokens[0].text != "node" || tokens[4].text != "eventlauncher") + { + Error = "unexpected layout of event launcher definition"; + return false; + } + apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}}); + return true; +} + std::string detect_eol(std::string const &Text, std::string const &Fallback) { auto const linebreak{Text.find('\n')}; @@ -732,6 +783,66 @@ save_result node_layers::save(std::vector const &Rootstatements) return fail(state.error); } } + auto const push_edit = [&](layer_handle const Layer, source_span const &Span, std::string Text, basic_node const *Node) { + file_patch::edit edit; + edit.begin = Span.begin; + edit.end = Span.end; + edit.length = Text.size(); + edit.text = std::move(Text); + edit.node = Node; + state.patches[Layer].edits.emplace_back(std::move(edit)); + }; + // traction and event launchers are rewritten when the editor moved them along with the track they belong to + std::vector patchednodes; + auto const patch_node = [&](basic_node *Node, auto const &Patch) { + auto const lookup{m_sources.find(Node)}; + if (lookup == m_sources.end() || false == Node->dirty()) + { + return true; + } + auto const &source{lookup->second}; + if (false == is_output(resolve(source.layer)) || false == writable(source.layer)) + { + return true; + } + if (false == load(source.layer)) + { + return false; + } + auto const &content{state.content[source.layer]}; + if (source.span.end > static_cast(content.size())) + { + state.error = "definition of \"" + Node->name() + "\" is out of bounds of file \"" + layer(source.layer).name + "\""; + return false; + } + auto text{content.substr(static_cast(source.span.begin), static_cast(source.span.end - source.span.begin))}; + std::string error; + if (false == Patch(text, source, error)) + { + state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; + return false; + } + push_edit(source.layer, source.span, std::move(text), Node); + rewritten.emplace(Node); + patchednodes.push_back(Node); + return true; + }; + for (auto *traction : simulation::Traction.sequence()) + { + if (traction != nullptr && + false == patch_node(traction, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_traction(Text, *traction, Source.context.offset, Error); })) + { + return fail(state.error); + } + } + for (auto *launcher : simulation::Events.launchers()) + { + if (launcher != nullptr && + false == patch_node(launcher, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_launcher(Text, Source.context.to_local(launcher->location()), Error); })) + { + return fail(state.error); + } + } std::vector savedpaths; std::vector droppedpaths; auto const path_text = [](TTrack &Path, glm::dvec3 const &Offset) { @@ -778,21 +889,16 @@ save_result node_layers::save(std::vector const &Rootstatements) { return fail(state.error); } - file_patch::edit edit; - edit.begin = source.span.begin; - edit.end = source.span.end; if (path->m_editorremoved) { + push_edit(source.layer, source.span, {}, nullptr); droppedpaths.push_back(path); } else { - edit.text = path_text(*path, source.context.offset); - edit.length = edit.text.size(); - edit.node = path; + push_edit(source.layer, source.span, path_text(*path, source.context.offset), path); savedpaths.push_back(path); } - state.patches[source.layer].edits.emplace_back(std::move(edit)); rewritten.emplace(path); } // roads. the lanes aren't saved, they're generated from the definition of the road @@ -994,6 +1100,33 @@ save_result node_layers::save(std::vector const &Rootstatements) state.patches[erased.first].edits.emplace_back(std::move(edit)); } + // editor data in the comment lines: the old lines go, the new ones are added to the file like new definitions + std::map> markedtext; + for (auto const &marking : m_marking) + { + auto const markedlayer{marking.first.first}; + auto const &mark{marking.first.second}; + if (auto const lookup{m_marked.find(markedlayer)}; lookup != m_marked.end()) + { + for (auto const &line : lookup->second) + { + if (line.mark == mark) + { + push_edit(markedlayer, line.span, {}, nullptr); + } + } + } + auto const target{resolve(markedlayer)}; + if (marking.second.empty() || false == is_output(target)) + { + continue; + } + for (auto const &text : marking.second) + { + markedtext[target].push_back(mark + ' ' + text); + } + } + // includes of templates std::map> placed; // directives made in the editor, by target layer for (std::size_t idx = 0; idx < m_instances.size(); ++idx) @@ -1133,7 +1266,7 @@ save_result node_layers::save(std::vector const &Rootstatements) continue; } if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || placed.count(candidate) != 0 || - (candidate == root && false == Rootstatements.empty())) + markedtext.count(candidate) != 0 || (candidate == root && false == Rootstatements.empty())) { outputs.emplace_back(candidate); } @@ -1227,6 +1360,14 @@ save_result node_layers::save(std::vector const &Rootstatements) added.text += directive + eol; } } + if (auto const lookup{markedtext.find(output)}; lookup != markedtext.end()) + { + for (auto const &line : lookup->second) + { + ensure_newline(added.text, eol); + added.text += line + eol; + } + } if (output == root) { for (auto const &statement : Rootstatements) @@ -1414,7 +1555,17 @@ save_result node_layers::save(std::vector const &Rootstatements) for (auto const output : written) { stat(output); + scan(output); } + for (auto const &marking : m_marking) + { + if (resolve(marking.first.first) != marking.first.first) + { + // the lines of the merged layer are now in the file of its target + m_marked.erase(marking.first.first); + } + } + m_marking.clear(); for (auto &included : m_instances) { // removed includes are gone from the files now @@ -1425,6 +1576,10 @@ save_result node_layers::save(std::vector const &Rootstatements) { path->m_dirty = false; } + for (auto *node : patchednodes) + { + node->m_dirty = false; + } for (auto const *path : droppedpaths) { m_sources.erase(path); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 7082bfc8..223d7f0f 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -587,6 +587,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), traction ); simulation::Region->insert_and_register( traction ); + scene::Layers.track( traction, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "tractionpowersource" ) { @@ -733,6 +734,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch simulation::Region->insert( eventlauncher ); } } + scene::Layers.track( eventlauncher, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "sound" ) { diff --git a/world/Event.h b/world/Event.h index 52ee3468..9b8f32d8 100644 --- a/world/Event.h +++ b/world/Event.h @@ -699,6 +699,14 @@ public: // returns all eventlaunchers in radius ignoring height std::vector find_eventlaunchers(glm::vec2 center, float radius) const; + // all event launchers, global ones included + std::vector + launchers() const { + auto const &input { m_inputdrivenlaunchers.sequence() }; + auto const &radio { m_radiodrivenlaunchers.sequence() }; + std::vector result( input.begin(), input.end() ); + result.insert( result.end(), radio.begin(), radio.end() ); + return result; } private: // types