From cdd5c242d1ccddf6b521abdb83f978be43220801 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:14:03 +0200 Subject: [PATCH 01/34] feat(editor): curves take over the adjoining straights --- application/editormode.h | 1 + application/editormodetrack.cpp | 112 ++++++++++++++++++++++++------ editor/editorAlignment.cpp | 89 ++++++++++++++++++------ editor/editorAlignment.hpp | 7 ++ editor/editorTrack.cpp | 116 ++++++++++++++++++++++++++++++++ editor/editorTrack.hpp | 10 +++ 6 files changed, 297 insertions(+), 38 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 262b1b1b..19438feb 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -313,6 +313,7 @@ class editor_mode : public application_mode }; void render_route_ui(); 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(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index e8adfae1..8ebd64f9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -433,7 +433,17 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve 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 = {}; @@ -818,6 +828,24 @@ void editor_mode::route_recommend(alignment::vertex &Vertex) const Vertex.vertical_radius = 0.0; } +void editor_mode::route_bind_ends() +{ + auto const &chain{m_route.chain}; + auto &design{m_route.design}; + 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 = chain.start; + design.end = chain.end; + 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.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() { auto &route{m_route}; @@ -837,18 +865,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)) @@ -938,16 +958,64 @@ 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); + auto const plandistance = [](glm::dvec3 const &A, glm::dvec3 const &B) { return glm::distance(glm::dvec2{A.x, A.z}, glm::dvec2{B.x, B.z}); }; + std::string error; + if (false == editor_track::find_chain(from, to, chain, error) || plandistance(chain.start, route.result.start) > 5.0 || plandistance(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())}; + 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) + { + char text[96]; + std::snprintf(text, sizeof(text), ", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); + route.status += text; + } WriteLog("Editor: route design - " + route.status, logtype::generic); } @@ -1033,11 +1101,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); @@ -1062,7 +1132,7 @@ void editor_mode::draw_route_overlay() const 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)) @@ -1369,7 +1439,10 @@ void editor_mode::render_route_ui() 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) @@ -2165,6 +2238,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 +2259,7 @@ 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())}; auto created{editor_track::relay(m_route.chain, pieces)}; editor_track::commit(tracks); for (auto const &entry : splitstates) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 703683ce..b93046b9 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -277,11 +277,18 @@ 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)}; @@ -351,6 +358,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 +458,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 +551,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}; @@ -615,13 +629,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; @@ -698,8 +709,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; @@ -757,6 +775,39 @@ 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)}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index f8628170..a3e4572f 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -65,6 +65,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}; @@ -152,6 +155,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/editorTrack.cpp b/editor/editorTrack.cpp index 73ebb580..6df8d41b 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -27,6 +27,7 @@ http: #include #include #include +#include #include namespace @@ -1179,6 +1180,121 @@ 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); +} + +double plan_length(segment_data const &Path) +{ + return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])); +} + +} // 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 &far{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + if (run.tracks.empty()) + { + auto const own{glm::normalize(plan_of(far - joint))}; + if (std::abs(lateral_of(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0) + break; + direction = own; + } + auto const offset{plan_of(far) - origin}; + auto const along{glm::dot(offset, direction)}; + if (along <= run.length + 1e-3 || std::abs(lateral_of(direction, offset)) > Tolerance.offset) + break; + run.tracks.push_back(current); + run.outward.push_back(outward); + run.length = along; + joint = far; + 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(plan_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{plan_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; +} + std::vector editor_track::find_switch_templates() { std::vector result; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 45035212..13800f43 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -137,6 +137,16 @@ 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); struct switch_template { TTrack *source{nullptr}; From 7371b189106c0891e9b265cc07cf76ac480da9f1 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:14:03 +0200 Subject: [PATCH 02/34] feat(editor): vertical profile editor, grade lines kept in the scenery --- CMakeLists.txt | 2 + application/editormode.cpp | 4 + application/editormode.h | 68 ++ application/editormodeprofile.cpp | 1189 +++++++++++++++++++++++++++++ application/editormodetrack.cpp | 73 +- application/editorprojection.h | 63 ++ editor/editorAlignment.cpp | 14 - editor/editorProfile.cpp | 422 ++++++++++ editor/editorProfile.hpp | 119 +++ editor/editorTrack.cpp | 480 ++++++++++++ editor/editorTrack.hpp | 45 ++ scene/scenelayers.cpp | 116 ++- scene/scenelayers.h | 21 + scene/scenelayerssave.cpp | 51 +- 14 files changed, 2591 insertions(+), 76 deletions(-) create mode 100644 application/editormodeprofile.cpp create mode 100644 application/editorprojection.h create mode 100644 editor/editorProfile.cpp create mode 100644 editor/editorProfile.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fe5b1f2b..8900a0be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -172,10 +172,12 @@ set(SOURCES "editor/editorTerrain.cpp" "editor/editorTrack.cpp" "editor/editorAlignment.cpp" +"editor/editorProfile.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" +"application/editormodeprofile.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index dd8bafa2..3b83664d 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1021,6 +1021,8 @@ bool editor_mode::update() draw_build_overlay(); draw_track_hints(); render_track_window(); + draw_profile_overlay(); + render_profile_window(); } // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- @@ -1633,6 +1635,8 @@ void editor_mode::save() } } + if (m_profile.changed) + profile_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 19438feb..b517736c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorOrthophoto.hpp" #include "editor/editorTrack.hpp" #include "editor/editorAlignment.hpp" +#include "editor/editorProfile.hpp" #include #include @@ -477,6 +478,73 @@ class editor_mode : public application_mode void toggle_straight_set(TTrack &Track); 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 profile_open(TTrack *From, TTrack *To); + void profile_open_run(TTrack &Track); + void profile_resample(); + void profile_recognize(); + void profile_check(); + void profile_apply(); + void profile_fit_view(); + void profile_after_undo(); + void render_profile_canvas(); + void render_profile_point(); + void draw_profile_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/editormodeprofile.cpp b/application/editormodeprofile.cpp new file mode 100644 index 00000000..8a6aa1e6 --- /dev/null +++ b/application/editormodeprofile.cpp @@ -0,0 +1,1189 @@ +/* +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 "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 +{ + +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)}; + +template +std::string format(char const *Format, Args... Arguments) +{ + char text[256]; + std::snprintf(text, sizeof(text), Format, Arguments...); + return text; +} + +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; +} + +glm::dvec3 position_at(std::vector const &Samples, double const Chainage) +{ + 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 Samples.front().position; + if (next == Samples.end()) + return Samples.back().position; + auto const &before{*std::prev(next)}; + auto const run{next->chainage - before.chainage}; + return run > 1e-9 ? glm::mix(before.position, next->position, (Chainage - before.chainage) / run) : before.position; +} + +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"}; + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +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{position_at(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 = position_at(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(position_at(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; + 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_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; + } + state.route = std::move(route); + state.from = state.picked_from = state.route.spans.front().track; + state.to = state.picked_to = state.route.spans.back().track; + 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; + } + 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(); + } + 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"); + } + + 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"); + if (changed) + profile_edited(); + + render_profile_canvas(); + + ImGui::Columns(2, "##profilecolumns", true); + render_profile_point(); + ImGui::NextColumn(); + 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(); + ImGui::Columns(1); + ImGui::End(); +} + +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(); +} + +void editor_mode::render_profile_canvas() +{ + auto &state{m_profile}; + auto &line{state.line}; + auto &points{line.points}; + auto const &samples{state.samples}; + ImGuiIO const &io{ImGui::GetIO()}; + + 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()}; + ImDrawList *draw{ImGui::GetWindowDrawList()}; + + float const strip{state.show_plan ? 44.0f : 0.0f}; + float const top{18.0f}; + float const height{size.y - strip - top}; + auto const width{std::max(1.0, state.view_to - state.view_from)}; + auto const across{width / size.x}; + if (state.exaggeration <= 0.0f) + { + double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; + for (auto const &sample : samples) + { + low = std::min(low, sample.position.y); + high = std::max(high, sample.position.y); + } + for (auto const &point : 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(across / (range / height), 1.0, 500.0)); + state.view_centre = (low + high) * 0.5; + } + auto const vertical{across / state.exaggeration}; + auto const x_of = [&](double const Chainage) { return corner.x + static_cast((Chainage - state.view_from) / across); }; + auto const y_of = [&](double const Height) { return corner.y + top + height * 0.5f - static_cast((Height - state.view_centre) / vertical); }; + auto const chainage_of = [&](float const X) { return state.view_from + (X - corner.x) * across; }; + auto const height_of = [&](float const Y) { return state.view_centre - (Y - corner.y - top - height * 0.5f) * vertical; }; + ImVec2 const mouse{io.MousePos}; + + 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{chainage_of(mouse.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 * across; + state.view_to -= io.MouseDelta.x * across; + state.view_centre += io.MouseDelta.y * vertical; + } + else + state.panning = false; + } + + auto const curves{profile::curves(line)}; + 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; + auto const dx{x_of(points[i].chainage) - mouse.x}; + auto const dy{y_of(points[i].elevation) - mouse.y}; + if (dx * dx + dy * dy < best) + { + best = dx * dx + dy * dy; + hit = i; + } + } + return hit; + }; + auto const hit_grip = [&]() { + for (auto const &c : curves) + { + if (points[c.point].joint) + continue; + auto const chainage{points[c.point].chainage}; + auto const dx{x_of(chainage) - mouse.x}; + auto const dy{y_of(profile::elevation(line, chainage)) - mouse.y}; + if (dx * dx + dy * dy < 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{chainage_of(mouse.x)}; + if (chainage > points.front().chainage + 0.5 && chainage < points.back().chainage - 0.5 && std::abs(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(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(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{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; + } + if (changed) + profile_edited(); + state.hover = hovered ? chainage_of(mouse.x) : -1.0; + + ImVec2 const bottomright{corner.x + size.x, corner.y + size.y}; + draw->AddRectFilled(corner, bottomright, IM_COL32(22, 24, 28, 255)); + draw->PushClipRect(corner, bottomright, true); + auto const plotbottom{corner.y + top + height}; + + auto const step{nice_step(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{x_of(s)}; + draw->AddLine(ImVec2(x, corner.y + top), ImVec2(x, plotbottom), kGrid); + draw->AddText(ImVec2(x + 2.0f, corner.y + 2.0f), kLabel, profile::format_chainage(s + state.origin).c_str()); + } + auto const heightstep{nice_step(vertical * 45.0)}; + for (auto h = std::floor(height_of(plotbottom) / heightstep) * heightstep; h <= height_of(corner.y + top); h += heightstep) + { + auto const y{y_of(h)}; + draw->AddLine(ImVec2(corner.x, y), ImVec2(bottomright.x, y), kGrid); + draw->AddText(ImVec2(corner.x + 2.0f, y - 14.0f), kLabel, format("%.1f", h).c_str()); + } + for (auto const &zone : state.context.switches) + { + draw->AddRectFilled(ImVec2(x_of(zone.from), corner.y + top), ImVec2(std::max(x_of(zone.to), x_of(zone.from) + 2.0f), plotbottom), kSwitch); + draw->AddText(ImVec2(x_of(zone.from) + 2.0f, corner.y + top + 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{x_of(samples[i].chainage)}; + auto const h{Height(i)}; + if (std::isnan(h) || x < 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, 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(x_of(points[i].chainage), y_of(points[i].elevation)), ImVec2(x_of(points[i + 1].chainage), y_of(points[i + 1].elevation)), kTangents, 1.0f); + { + auto const from{std::max(points.front().chainage, chainage_of(corner.x))}; + auto const to{std::min(points.back().chainage, chainage_of(bottomright.x))}; + ImVec2 previous{x_of(from), y_of(profile::elevation(line, from))}; + for (auto x = previous.x + 2.0f; x <= x_of(to) + 2.0f; x += 2.0f) + { + auto const chainage{std::min(to, chainage_of(x))}; + ImVec2 const next{x_of(chainage), 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{x_of(middle)}; + if (x_of(points[i + 1].chainage) - 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, y_of(profile::elevation(line, middle)) - 30.0f), kGradeLine, text.c_str()); + } + for (auto const &c : curves) + { + for (auto const at : {c.start, c.end}) + { + auto const y{y_of(profile::elevation(line, at))}; + draw->AddLine(ImVec2(x_of(at), y - 7.0f), ImVec2(x_of(at), y + 7.0f), kGradeLine, 1.5f); + } + auto const chainage{points[c.point].chainage}; + ImVec2 const grip{x_of(chainage), y_of(profile::elevation(line, chainage))}; + draw->AddCircleFilled(grip, 4.5f, IM_COL32(60, 230, 90, 255), 4); + if (static_cast(c.point) == state.curve_grip) + draw->AddCircle(grip, 9.0f, IM_COL32(255, 255, 255, 255), 12, 2.0f); + draw->AddText(ImVec2(grip.x + 7.0f, grip.y + 4.0f), IM_COL32(60, 230, 90, 255), format("R %.0f", points[c.point].radius).c_str()); + } + for (int i = 0; i < static_cast(points.size()); ++i) + { + ImVec2 const at{x_of(points[i].chainage), 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, IM_COL32(255, 210, 60, 255)); + if (i == state.selected) + draw->AddCircle(at, 10.0f, IM_COL32(255, 255, 255, 255), 16, 2.0f); + draw->AddText(ImVec2(at.x + 7.0f, at.y - 18.0f), IM_COL32(255, 210, 60, 255), format("%d %.3f", i + 1, points[i].elevation).c_str()); + } + for (auto const &issue : state.issues) + { + auto const x{x_of(issue.chainage)}; + auto const colour{issue.error ? kError : kWarning}; + draw->AddTriangleFilled(ImVec2(x - 5.0f, corner.y + top), ImVec2(x + 5.0f, corner.y + top), ImVec2(x, corner.y + top + 8.0f), colour); + } + if (state.show_plan && false == samples.empty()) + { + auto const middle{plotbottom + strip * 0.5f}; + draw->AddLine(ImVec2(corner.x, plotbottom), ImVec2(bottomright.x, plotbottom), IM_COL32(255, 255, 255, 60)); + draw->AddLine(ImVec2(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{x_of(samples[i].chainage)}; + if (x < 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 * strip * 0.4), -strip * 0.45f, 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(corner.x + 2.0f, plotbottom + 1.0f), kLabel, "plan: curvature (up: left)"); + } + if (hovered && state.hover >= 0.0 && state.hover <= state.route.length && false == samples.empty()) + { + auto const x{x_of(state.hover)}; + draw->AddLine(ImVec2(x, corner.y + top), ImVec2(x, bottomright.y), IM_COL32(255, 255, 255, 90)); + auto const designed{profile::elevation(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(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(); + } + draw->PopClipRect(); +} + +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{position_at(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, IM_COL32(255, 210, 60, 255)); + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 16.0f), IM_COL32(255, 210, 60, 255), format("%d", i + 1).c_str()); + } + if (state.hover >= 0.0 && state.hover <= state.route.length) + { + ImVec2 screen; + if (projection.project(position_at(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 8ebd64f9..ed5ecd30 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -11,6 +11,7 @@ http: #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" +#include "application/editorprojection.h" #include "utilities/Globals.h" #include "rendering/renderer.h" @@ -31,54 +32,6 @@ http: namespace { -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); - } - - 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]; @@ -452,6 +405,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() @@ -459,6 +413,13 @@ 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; + } if (ImGui::CollapsingHeader("Straights in the scenery")) render_straights_ui(); render_switch_ui(); @@ -507,6 +468,8 @@ void editor_mode::render_track_window() if (ImGui::SmallButton("Edit points and parameters of the single path")) m_track_tab = track_tab::path; } + if (ImGui::SmallButton("Vertical profile along this line") && selected_track() != nullptr) + profile_open_run(*selected_track()); ImGui::End(); } @@ -990,6 +953,7 @@ void editor_mode::route_apply() 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()); @@ -1232,11 +1196,6 @@ void editor_mode::render_route_gizmo() else vertex.position = point; } - if (std::abs(moved.y - position.y) > 1e-3) - { - vertex.auto_elevation = false; - vertex.elevation = moved.y; - } route_update(); } @@ -1308,8 +1267,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); @@ -1401,16 +1358,11 @@ void editor_mode::render_route_ui() 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); @@ -2260,6 +2212,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co return; } 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) diff --git a/application/editorprojection.h b/application/editorprojection.h new file mode 100644 index 00000000..aa5cd45d --- /dev/null +++ b/application/editorprojection.h @@ -0,0 +1,63 @@ +/* +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 "imgui/imgui.h" + +// 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); + } + + private: + static constexpr float kNear{0.1f}; + ImVec2 m_size; + glm::dvec3 m_camera; + glm::mat4 m_viewprojection; +}; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index b93046b9..c2940c2e 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -752,20 +752,6 @@ result fit_between(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) diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp new file mode 100644 index 00000000..f093dee4 --- /dev/null +++ b/editor/editorProfile.cpp @@ -0,0 +1,422 @@ +/* +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 +#include +#include +#include + +namespace profile +{ + +namespace +{ + +template +std::string format(char const *Format, Args... Arguments) +{ + char text[256]; + std::snprintf(text, sizeof(text), Format, Arguments...); + return text; +} + +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 6df8d41b..a579defe 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1295,6 +1295,486 @@ TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm:: return edge; } +namespace +{ + +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(point(0.0))}; + 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) const + { + int constexpr steps{256}; + double length{0.0}; + auto previous{plan_of(point(0.0))}; + 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; + } +}; + +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]}; + bezier const curve{span.track->m_paths[span.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; + if (planar > 1e-9) + { + sample.direction = glm::dvec2{derivative.x, derivative.z} / 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 +double interpolate(std::vector const &Samples, double const Chainage, Getter const &Get) +{ + if (Samples.empty()) + return 0.0; + 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(glm::dvec2{Point.x - Origin.x, Point.z - Origin.z}, 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; }); +} + +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; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 13800f43..396a66c5 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -147,6 +147,51 @@ class editor_track 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 + 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); + // 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}; 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..523e2da3 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); @@ -319,9 +330,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 +354,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 df077aa7..9d5f9297 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -801,6 +801,37 @@ 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) + { + continue; + } + file_patch::edit edit; + edit.begin = line.span.begin; + edit.end = line.span.end; + state.patches[markedlayer].edits.emplace_back(std::move(edit)); + } + } + 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) @@ -940,7 +971,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); } @@ -1034,6 +1065,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) @@ -1221,7 +1260,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 From 8778353b221be8ec2eb70df4bfa676cdcb176836 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:14:03 +0200 Subject: [PATCH 03/34] feat(editor): infrastructure bound to the paths follows their changes --- CMakeLists.txt | 2 + application/editormode.cpp | 7 + application/editormode.h | 54 +- application/editormodeinfra.cpp | 1390 ++++++++++++++++++++++ application/editormodetrack.cpp | 20 + editor/editorInfra.cpp | 304 +++++ editor/editorInfra.hpp | 128 ++ editor/editorTrack.cpp | 13 + editor/editorTrack.hpp | 7 + scene/scene.cpp | 24 + scene/scene.h | 8 + scene/scenelayers.h | 5 + scene/scenelayerssave.cpp | 112 ++ simulation/simulationstateserializer.cpp | 2 + world/Event.h | 8 + 15 files changed, 2083 insertions(+), 1 deletion(-) create mode 100644 application/editormodeinfra.cpp create mode 100644 editor/editorInfra.cpp create mode 100644 editor/editorInfra.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8900a0be..94d71aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,11 +173,13 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorAlignment.cpp" "editor/editorProfile.cpp" +"editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormodeprofile.cpp" +"application/editormodeinfra.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 3b83664d..c4cedb68 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -344,6 +344,7 @@ 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); }); + infra_hooks(); } editor_ui *editor_mode::ui() const @@ -869,6 +870,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(); @@ -1023,6 +1027,8 @@ bool editor_mode::update() render_track_window(); draw_profile_overlay(); render_profile_window(); + draw_infra_overlay(); + render_infra_window(); } // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- @@ -1637,6 +1643,7 @@ 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 b517736c..082682e4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -22,6 +22,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTrack.hpp" #include "editor/editorAlignment.hpp" #include "editor/editorProfile.hpp" +#include "editor/editorInfra.hpp" #include #include @@ -107,7 +108,7 @@ class editor_mode : public application_mode std::vector> tracks; std::vector created; std::vector removed; - + 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()); @@ -545,6 +546,57 @@ class editor_mode : public application_mode 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}; + bool m_bindings_changed{false}; + std::vector m_infra_buffer; // states of the objects before the change in progress + bool m_infra_suspended{false}; + void infra_hooks(); + 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); + void infra_capture(TTrack const &Track); + void infra_follow(std::vector const &Tracks); + void infra_split(TTrack &Original, TTrack &Created, segment_data const &First); + void infra_move(infra::binding &Binding); + void infra_attach(EditorSnapshot &Snapshot); + void render_infra_window(); + 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/editormodeinfra.cpp b/application/editormodeinfra.cpp new file mode 100644 index 00000000..0fb987e5 --- /dev/null +++ b/application/editormodeinfra.cpp @@ -0,0 +1,1390 @@ +/* +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/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 +{ + +std::string const kBindingMark{"//$b"}; + +template +std::string format(char const *Format, Args... Arguments) +{ + char text[256]; + std::snprintf(text, sizeof(text), Format, Arguments...); + return text; +} + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +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(glm::dvec2{Point.x - axis.point.x, Point.z - axis.point.z}, 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; +} + +bool station_near(std::vector const &Areas, glm::dvec3 const &Point, double const Corridor, infra::station &Station) +{ + return nearest_beside(Areas, Point, Corridor, Station); +} + +// 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)); +} + +} // namespace + +void editor_mode::infra_hooks() +{ + editor_track::on_capture = [this](TTrack const &Track) { infra_capture(Track); }; + editor_track::on_commit = [this](std::vector const &Tracks) { infra_follow(Tracks); }; + editor_track::on_retire = [this](TTrack &) { infra_follow({}); }; + editor_track::on_split = [this](TTrack &Original, TTrack &Created, segment_data const &First) { infra_split(Original, Created, First); }; +} + +// 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 +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; + + point_grid paths; + 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); + point_grid models; + for (auto *model : simulation::Instances.sequence()) + if (model != nullptr && false == model->from_template() && model->Model() != nullptr) + models.add(model->location(), model); + point_grid traction; + for (auto *piece : simulation::Traction.sequence()) + if (piece != nullptr && false == piece->from_template()) + traction.add(piece->pPoint1, piece); + point_grid logic; + 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); + point_grid includes; + 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); + } + + double const tolerance{0.05}; + 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; + 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; + auto const keep = [&]() { m_bindings_unresolved.emplace_back(layer, text); }; + if (line.fail() || count < 1 || count > 4) + { + keep(); + continue; + } + 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{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{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{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{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)) + { + keep(); + continue; + } + 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{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)]}; + double s; + bool interior; + infra::project((*track)->m_paths[path], point, s, interior); + binding.anchors.push_back(infra::make_anchor({*track, path, s}, point)); + } + if (false == found || binding.anchors.size() != points.size()) + { + keep(); + continue; + } + binding.turns = binding.turns && yaw.has_value(); + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + 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)); + } + m_bindings_changed = false; +} + +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; + } + if (count > 0) + m_bindings_changed = true; + 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); + } + if (false == Indices.empty()) + m_bindings_changed = true; +} + +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::infra_capture(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; + double s; + bool interior; + infra::project(path, points[i], s, interior); + anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, 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::infra_follow(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 (station_near(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 station_near(*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::infra_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) + { + // what changed in the meantime becomes the new starting point + 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; + } + double s; + bool interior; + infra::project(anchor.at.track->m_paths[anchor.at.path], points[i], s, interior); + anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, points[i]); + } + if (binding.turns && yaw.has_value()) + binding.yaw = turn_of(binding, *yaw); + } + } + } + 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(); + 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(); + + auto &candidates{state.candidates}; + 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")) + for (auto &candidate : candidates) + candidate.chosen = (false == candidate.bound); + ImGui::SameLine(); + if (ImGui::SmallButton("None")) + for (auto &candidate : candidates) + candidate.chosen = 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())) + for (auto &candidate : candidates) + if (candidate.binding.group == group) + candidate.chosen = (false == candidate.bound); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{"none##g"} + infra::name(group)).c_str())) + for (auto &candidate : candidates) + if (candidate.binding.group == group) + candidate.chosen = 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(); + } + + 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(); + } + + 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(); +} + +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 ? IM_COL32(255, 60, 60, 255) : 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/editormodetrack.cpp b/application/editormodetrack.cpp index ed5ecd30..29a2ab9b 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -342,6 +342,7 @@ void editor_mode::push_track_snapshot(std::vectoruuid; snap.tracks = std::move(States); snap.created = std::move(Created); + infra_attach(snap); m_history.push_back(std::move(snap)); g_redo.clear(); } @@ -354,6 +355,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}; @@ -361,6 +363,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) @@ -383,6 +389,10 @@ 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; @@ -420,6 +430,9 @@ void editor_mode::render_track_ui() 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(); @@ -470,6 +483,13 @@ void editor_mode::render_track_window() } 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")) + { + m_infra.open = true; + m_infra.scope = 1; + infra_recognize(); + } ImGui::End(); } diff --git a/editor/editorInfra.cpp b/editor/editorInfra.cpp new file mode 100644 index 00000000..974b5689 --- /dev/null +++ b/editor/editorInfra.cpp @@ -0,0 +1,304 @@ +/* +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 "world/Track.h" + +#include +#include +#include +#include + +namespace infra +{ + +namespace +{ + +int const kSteps{128}; + +struct curve +{ + glm::dvec3 p0, p1, p2, p3; +}; + +curve curve_of(segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{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{}) + return {start, start + (end - start) / 3.0, end + (start - end) / 3.0, end}; + return {start, start + control1, end + control2, end}; +} + +glm::dvec3 point(curve const &Curve, double const T) +{ + auto const u{1.0 - T}; + return Curve.p0 * (u * u * u) + Curve.p1 * (3.0 * u * u * T) + Curve.p2 * (3.0 * u * T * T) + Curve.p3 * (T * T * T); +} + +glm::dvec3 derivative(curve const &Curve, double const T) +{ + auto const u{1.0 - T}; + return (Curve.p1 - Curve.p0) * (3.0 * u * u) + (Curve.p2 - Curve.p1) * (6.0 * u * T) + (Curve.p3 - Curve.p2) * (3.0 * T * T); +} + +double plan(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +// arc length in the plan at the evenly spread parameters +std::array lengths(curve const &Curve) +{ + std::array result{}; + auto previous{Curve.p0}; + for (int i = 1; i <= kSteps; ++i) + { + auto const next{point(Curve, static_cast(i) / kSteps)}; + result[i] = result[i - 1] + plan(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(curve const &Curve, double const T) +{ + auto tangent{derivative(Curve, 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(curve_of(Path)).back(); +} + +std::vector outline(segment_data const &Path, double const Step) +{ + auto const shape{curve_of(Path)}; + // the speed along the curve doesn't exceed three times the longest leg of the control polygon + auto const leg{std::max({plan(shape.p0, shape.p1), plan(shape.p1, shape.p2), plan(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(point(shape, static_cast(i) / count)); + return result; +} + +frame frame_at(segment_data const &Path, double const S) +{ + auto const shape{curve_of(Path)}; + auto const t{parameter(lengths(shape), S)}; + return {point(shape, 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) +{ + auto const shape{curve_of(Path)}; + auto const table{lengths(shape)}; + auto const distance = [&](double const T) { return plan(point(shape, 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(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(glm::dvec2{Point.x - axis.point.x, Point.z - axis.point.z}, 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..afb76d8e --- /dev/null +++ b/editor/editorInfra.hpp @@ -0,0 +1,128 @@ +/* +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); +// 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/editorTrack.cpp b/editor/editorTrack.cpp index a579defe..f86f012f 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -124,8 +124,15 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool } +std::function editor_track::on_capture; +std::function const &)> editor_track::on_commit; +std::function editor_track::on_retire; +std::function editor_track::on_split; + editor_track::state editor_track::capture(TTrack const &Track) { + if (on_capture) + on_capture(Track); state result; result.paths = Track.m_paths; result.velocity = Track.fVelocity; @@ -235,6 +242,8 @@ void editor_track::commit(std::vector const &Tracks) for (auto *track : affected) for (auto *vehicle : track->Dynamics) vehicle->Move(0.000001); + if (on_commit) + on_commit(Tracks); } void editor_track::commit_parameters(TTrack &Track) @@ -2300,6 +2309,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 (on_split) + on_split(Track, *created, first); commit({&Track, created}); return created; } @@ -2336,6 +2347,8 @@ void editor_track::retire(TTrack &Track) update_transition(*neighbour); rebuild_geometry(*neighbour); } + if (on_retire) + on_retire(Track); } void editor_track::revive(TTrack &Track) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 396a66c5..34b0a7d8 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -227,6 +227,13 @@ class editor_track static void velocity(TTrack &Track, double const Velocity); static void damage(TTrack &Track, int const Damage); + // let the objects which belong to the paths follow their changes + static std::function on_capture; + static std::function const &)> on_commit; + static std::function on_retire; + // called before the commit; the first argument keeps the beginning of the path, given in the last one + static std::function on_split; + private: static void disconnect(TTrack &Track); static void join(TTrack &Track); 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.h b/scene/scenelayers.h index 523e2da3..28517c16 100644 --- a/scene/scenelayers.h +++ b/scene/scenelayers.h @@ -292,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 { diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 9d5f9297..02029a3a 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -14,6 +14,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" @@ -233,6 +236,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')}; @@ -730,6 +781,63 @@ save_result node_layers::save(std::vector const &Rootstatements) return fail(state.error); } } + // 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; + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + edit.length = text.size(); + edit.text = std::move(text); + edit.node = Node; + state.patches[source.layer].edits.emplace_back(std::move(edit)); + 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) { @@ -1281,6 +1389,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 18cd07af..da0813a8 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -510,6 +510,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" ) { @@ -656,6 +657,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 From e5c91d8a1a213c12ff996dbfb339b0e40c74830a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:33:44 +0200 Subject: [PATCH 04/34] refactor(editor): shared plan geometry and formatting helpers --- application/editormodeinfra.cpp | 20 +- application/editormodeprofile.cpp | 39 +--- application/editormodetrack.cpp | 344 ++++++++++++------------------ editor/editorAlignment.cpp | 50 ++--- editor/editorFormat.hpp | 26 +++ editor/editorGeometry.hpp | 131 ++++++++++++ editor/editorInfra.cpp | 66 ++---- editor/editorProfile.cpp | 9 +- editor/editorTrack.cpp | 188 +++++----------- editor/editorTrack.hpp | 6 + 10 files changed, 392 insertions(+), 487 deletions(-) create mode 100644 editor/editorFormat.hpp create mode 100644 editor/editorGeometry.hpp diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 0fb987e5..d81dd2f2 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -10,6 +10,8 @@ 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" @@ -40,21 +42,11 @@ http://mozilla.org/MPL/2.0/. namespace { +using geometry::plan_distance; +using geometry::plan_of; + std::string const kBindingMark{"//$b"}; -template -std::string format(char const *Format, Args... Arguments) -{ - char text[256]; - std::snprintf(text, sizeof(text), Format, Arguments...); - return text; -} - -double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) -{ - return std::hypot(A.x - B.x, A.z - B.z); -} - template bool contains(std::vector const &Items, Type_ const &Item) { @@ -357,7 +349,7 @@ bool beside(infra::station const &Station, glm::dvec3 const &Point, bool const I if (Interior) return true; auto const axis{infra::frame_at(Station)}; - return std::abs(glm::dot(glm::dvec2{Point.x - axis.point.x, Point.z - axis.point.z}, axis.forward)) <= 0.5; + 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 diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 8a6aa1e6..179ce97f 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -10,6 +10,8 @@ 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" @@ -27,6 +29,8 @@ http://mozilla.org/MPL/2.0/. 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)}; @@ -37,14 +41,6 @@ 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)}; -template -std::string format(char const *Format, Args... Arguments) -{ - char text[256]; - std::snprintf(text, sizeof(text), Format, Arguments...); - return text; -} - double nice_step(double const Approximate) { auto const power{std::pow(10.0, std::floor(std::log10(std::max(Approximate, 1e-6))))}; @@ -54,18 +50,6 @@ double nice_step(double const Approximate) return power * 10.0; } -glm::dvec3 position_at(std::vector const &Samples, double const Chainage) -{ - 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 Samples.front().position; - if (next == Samples.end()) - return Samples.back().position; - auto const &before{*std::prev(next)}; - auto const run{next->chainage - before.chainage}; - return run > 1e-9 ? glm::mix(before.position, next->position, (Chainage - before.chainage) / run) : before.position; -} - double route_speed(editor_track::route const &Route) { double speed{-1.0}; @@ -76,11 +60,6 @@ double route_speed(editor_track::route const &Route) std::string const kProfileMark{"//$p"}; -double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) -{ - return std::hypot(A.x - B.x, A.z - B.z); -} - profile::line reversed_line(profile::line Line, double const Length) { std::reverse(Line.points.begin(), Line.points.end()); @@ -224,7 +203,7 @@ int editor_mode::profile_find(bool &Reversed) const return -1; auto const start{state.samples.front().position}; auto const end{state.samples.back().position}; - auto const middle{position_at(state.samples, state.route.length * 0.5)}; + 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]}; @@ -274,7 +253,7 @@ void editor_mode::profile_store() entry.length = state.route.length; entry.start = state.samples.front().position; entry.end = state.samples.back().position; - entry.middle = position_at(state.samples, state.route.length * 0.5); + 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}; @@ -345,7 +324,7 @@ void editor_mode::profile_open_stored(std::size_t const Index) 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(position_at(samples, route.length * 0.5), entry.middle) > 0.5) + 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; @@ -1172,7 +1151,7 @@ void editor_mode::draw_profile_overlay() const auto const &points{state.line.points}; for (int i = 0; i < static_cast(points.size()); ++i) { - auto position{position_at(samples, points[i].chainage)}; + 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)) @@ -1183,7 +1162,7 @@ void editor_mode::draw_profile_overlay() const if (state.hover >= 0.0 && state.hover <= state.route.length) { ImVec2 screen; - if (projection.project(position_at(samples, state.hover), 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 29a2ab9b..ca5f7722 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -12,6 +12,8 @@ http: #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" @@ -32,18 +34,16 @@ http: namespace { -glm::dvec3 path_point(segment_data const &Path, double const T) +using geometry::bezier; +using geometry::cross; +using geometry::grade_of; +using geometry::plan_distance; +using geometry::plan_of; +using geometry::turned; + +std::string describe(editor_track::straight const &Line) { - 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) @@ -57,11 +57,6 @@ 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}; @@ -128,7 +123,7 @@ bool editor_mode::pick_track_handle() 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 = (dx * dx + dy * dy) * (editor_track::is_end(kind) ? 0.5f : 1.0f); if (distance < best) { best = distance; @@ -156,10 +151,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; } @@ -182,7 +177,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); @@ -247,7 +242,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))) { @@ -281,7 +276,7 @@ void editor_mode::render_track_gizmo() 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); @@ -501,9 +496,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]}; @@ -559,7 +554,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]); @@ -750,7 +745,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); @@ -815,14 +810,12 @@ void editor_mode::route_bind_ends() { auto const &chain{m_route.chain}; auto &design{m_route.design}; - 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 = chain.start; design.end = chain.end; - 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_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)); @@ -865,7 +858,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); @@ -908,7 +901,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}; } @@ -954,9 +947,8 @@ void editor_mode::route_apply() 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); - auto const plandistance = [](glm::dvec3 const &A, glm::dvec3 const &B) { return glm::distance(glm::dvec2{A.x, A.z}, glm::dvec2{B.x, B.z}); }; std::string error; - if (false == editor_track::find_chain(from, to, chain, error) || plandistance(chain.start, route.result.start) > 5.0 || plandistance(chain.end, route.result.end) > 5.0) + 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); @@ -995,11 +987,7 @@ void editor_mode::route_apply() } 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) - { - char text[96]; - std::snprintf(text, sizeof(text), ", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); - route.status += text; - } + route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); WriteLog("Editor: route design - " + route.status, logtype::generic); } @@ -1137,17 +1125,11 @@ void editor_mode::draw_route_overlay() const drawlist->AddCircleFilled(screen, 6.0f, IM_COL32(60, 230, 90, 255), 4); if (i == route.grip) drawlist->AddCircle(screen, 11.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f); - char radius[40]; 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), IM_COL32(60, 230, 90, 255), 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), IM_COL32(255, 210, 60, 255), format("W%d", i + 1).c_str()); } } @@ -1193,7 +1175,7 @@ void editor_mode::render_route_gizmo() { 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) @@ -1210,9 +1192,9 @@ 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; } @@ -1301,7 +1283,7 @@ void editor_mode::render_route_ui() 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; @@ -1341,9 +1323,7 @@ void editor_mode::render_route_ui() { 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)) + if (ImGui::Selectable(format("Vertex W%d", i + 1).c_str(), route.vertex == i)) route.vertex = (route.vertex == i ? -1 : i); ImGui::Indent(); ImGui::PushItemWidth(110.0f); @@ -1464,9 +1444,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()) @@ -1499,7 +1479,7 @@ void editor_mode::draw_straights_overlay() const drawlist->AddCircleFilled(screen, 7.0f, IM_COL32(255, 210, 60, 255)); 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)}; @@ -1510,7 +1490,7 @@ void editor_mode::draw_straights_overlay() const 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}; @@ -1568,9 +1548,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) @@ -1585,11 +1564,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); @@ -1623,8 +1599,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)); @@ -1643,7 +1619,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}; @@ -1694,11 +1670,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()); @@ -1730,11 +1701,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"); @@ -2011,7 +1977,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}; }; } @@ -2065,7 +2031,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; @@ -2074,9 +2040,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; }); @@ -2278,7 +2243,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; @@ -2322,7 +2287,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) { @@ -2531,7 +2496,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)) @@ -2547,7 +2512,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}; } } @@ -2576,24 +2541,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; @@ -2704,7 +2664,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; @@ -2758,13 +2718,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; }); @@ -2842,7 +2801,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}; @@ -2881,10 +2840,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; } @@ -2986,14 +2945,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; } @@ -3022,9 +2980,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]); @@ -3056,8 +3013,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; }); @@ -3086,10 +3043,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; @@ -3151,8 +3108,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)}; @@ -3193,20 +3149,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; @@ -3224,11 +3178,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; @@ -3243,16 +3197,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; @@ -3261,13 +3215,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]); @@ -3286,8 +3240,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; @@ -3297,18 +3249,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}; @@ -3339,7 +3286,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")); @@ -3357,14 +3304,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}; @@ -3380,19 +3325,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; @@ -3420,8 +3365,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)}; @@ -3453,7 +3398,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) @@ -3482,8 +3427,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) @@ -3513,7 +3456,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]}; @@ -3545,10 +3488,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}; @@ -3568,8 +3511,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); @@ -3603,8 +3546,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); } } @@ -3629,36 +3572,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; @@ -3698,8 +3625,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; @@ -3716,13 +3643,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; @@ -3738,8 +3665,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)}; @@ -3769,15 +3696,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}; @@ -3786,8 +3704,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) { @@ -3806,7 +3724,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) { diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index c2940c2e..bdd06cf3 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: @@ -290,8 +268,8 @@ result fit_between(design const &Design) 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()}; @@ -578,7 +556,7 @@ result fit_between(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; @@ -655,7 +633,7 @@ result fit_between(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); @@ -796,8 +774,8 @@ result compute(design const &Design) 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)}; @@ -813,7 +791,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/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/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 index 974b5689..a2fbc9ba 100644 --- a/editor/editorInfra.cpp +++ b/editor/editorInfra.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorInfra.hpp" +#include "editor/editorGeometry.hpp" #include "world/Track.h" #include @@ -23,50 +24,21 @@ namespace infra namespace { +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + int const kSteps{128}; -struct curve -{ - glm::dvec3 p0, p1, p2, p3; -}; - -curve curve_of(segment_data const &Path) -{ - auto const &start{Path.points[segment_data::point::start]}; - auto const &end{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{}) - return {start, start + (end - start) / 3.0, end + (start - end) / 3.0, end}; - return {start, start + control1, end + control2, end}; -} - -glm::dvec3 point(curve const &Curve, double const T) -{ - auto const u{1.0 - T}; - return Curve.p0 * (u * u * u) + Curve.p1 * (3.0 * u * u * T) + Curve.p2 * (3.0 * u * T * T) + Curve.p3 * (T * T * T); -} - -glm::dvec3 derivative(curve const &Curve, double const T) -{ - auto const u{1.0 - T}; - return (Curve.p1 - Curve.p0) * (3.0 * u * u) + (Curve.p2 - Curve.p1) * (6.0 * u * T) + (Curve.p3 - Curve.p2) * (3.0 * T * T); -} - -double plan(glm::dvec3 const &A, glm::dvec3 const &B) -{ - return std::hypot(A.x - B.x, A.z - B.z); -} - // arc length in the plan at the evenly spread parameters -std::array lengths(curve const &Curve) +std::array lengths(bezier const &Curve) { std::array result{}; auto previous{Curve.p0}; for (int i = 1; i <= kSteps; ++i) { - auto const next{point(Curve, static_cast(i) / kSteps)}; - result[i] = result[i - 1] + plan(previous, next); + auto const next{Curve.point(static_cast(i) / kSteps)}; + result[i] = result[i - 1] + plan_distance(previous, next); previous = next; } return result; @@ -92,9 +64,9 @@ double length_at(std::array const &Lengths, double const T) return Lengths[index] + (Lengths[index + 1] - Lengths[index]) * (position - index); } -glm::dvec2 plan_direction(curve const &Curve, double const T) +glm::dvec2 plan_direction(bezier const &Curve, double const T) { - auto tangent{derivative(Curve, 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}; @@ -144,27 +116,27 @@ std::vector const &categories() double plan_length(segment_data const &Path) { - return lengths(curve_of(Path)).back(); + return lengths(bezier{Path}).back(); } std::vector outline(segment_data const &Path, double const Step) { - auto const shape{curve_of(Path)}; + 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(shape.p0, shape.p1), plan(shape.p1, shape.p2), plan(shape.p2, shape.p3)})}; + 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(point(shape, static_cast(i) / count)); + result.push_back(shape.point(static_cast(i) / count)); return result; } frame frame_at(segment_data const &Path, double const S) { - auto const shape{curve_of(Path)}; + bezier const shape{Path}; auto const t{parameter(lengths(shape), S)}; - return {point(shape, t), plan_direction(shape, t)}; + return {shape.point(t), plan_direction(shape, t)}; } frame frame_at(station const &Station) @@ -176,9 +148,9 @@ frame frame_at(station const &Station) double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior) { - auto const shape{curve_of(Path)}; + bezier const shape{Path}; auto const table{lengths(shape)}; - auto const distance = [&](double const T) { return plan(point(shape, T), Point); }; + 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) @@ -256,7 +228,7 @@ anchor make_anchor(station const &At, glm::dvec3 const &Point) result.at = At; auto const axis{frame_at(At)}; glm::dvec2 const side{axis.forward.y, -axis.forward.x}; - result.offset = glm::dot(glm::dvec2{Point.x - axis.point.x, Point.z - axis.point.z}, side); + 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())) diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp index f093dee4..3ea7e13b 100644 --- a/editor/editorProfile.cpp +++ b/editor/editorProfile.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorProfile.hpp" +#include "editor/editorFormat.hpp" #include #include @@ -21,14 +22,6 @@ namespace profile namespace { -template -std::string format(char const *Format, Args... Arguments) -{ - char text[256]; - std::snprintf(text, sizeof(text), Format, Arguments...); - return text; -} - double polyline(line const &Line, double const Chainage) { auto const &points{Line.points}; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index f86f012f..c40c2dcd 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" @@ -33,7 +35,14 @@ http: 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) { @@ -46,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); @@ -75,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; @@ -391,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(); } @@ -454,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); @@ -784,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])}; @@ -815,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))}; @@ -823,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; } @@ -848,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)) @@ -935,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; @@ -949,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; @@ -1198,11 +1174,6 @@ double split_margin(double const Length) return std::max(0.5, 0.002 * Length); } -double plan_length(segment_data const &Path) -{ - return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])); -} - } // namespace editor_track::straight_run editor_track::straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance) @@ -1237,13 +1208,13 @@ editor_track::straight_run editor_track::straight_beyond(chain const &Chain, boo if (run.tracks.empty()) { auto const own{glm::normalize(plan_of(far - joint))}; - if (std::abs(lateral_of(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0) + if (std::abs(cross(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0) break; direction = own; } auto const offset{plan_of(far) - origin}; auto const along{glm::dot(offset, direction)}; - if (along <= run.length + 1e-3 || std::abs(lateral_of(direction, offset)) > Tolerance.offset) + if (along <= run.length + 1e-3 || std::abs(cross(direction, offset)) > Tolerance.offset) break; run.tracks.push_back(current); run.outward.push_back(outward); @@ -1258,7 +1229,7 @@ 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(plan_length(Run.tracks.back()->m_paths.front())) - 0.01); + 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) @@ -1271,7 +1242,7 @@ TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm:: auto *track{Run.tracks[i]}; auto const outward{Run.outward[i]}; auto const &path{track->m_paths.front()}; - auto const pathlength{plan_length(path)}; + auto const pathlength{chord_length(path)}; auto const margin{split_margin(pathlength)}; auto const remaining{length - covered}; if (remaining <= margin) @@ -1307,71 +1278,6 @@ TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm:: namespace { -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(point(0.0))}; - 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) const - { - int constexpr steps{256}; - double length{0.0}; - auto previous{plan_of(point(0.0))}; - 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; - } -}; - using route_node = std::tuple; std::vector route_next(route_node const &Node) @@ -1524,7 +1430,7 @@ std::vector editor_track::sample_route(route &Route, sample.span = i; if (planar > 1e-9) { - sample.direction = glm::dvec2{derivative.x, derivative.z} / planar; + sample.direction = plan_of(derivative) / planar; sample.grade = derivative.y / planar; sample.curvature = (derivative.x * second.z - derivative.z * second.x) / (planar * planar * planar); } @@ -1540,10 +1446,10 @@ namespace { template -double interpolate(std::vector const &Samples, double const Chainage, Getter const &Get) +auto interpolate(std::vector const &Samples, double const Chainage, Getter const &Get) -> decltype(Get(Samples.front())) { if (Samples.empty()) - return 0.0; + 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()); @@ -1583,7 +1489,7 @@ void plane_heights(segment_data &Path, glm::dvec3 const &Origin, glm::dvec2 cons control1 = (end - start) / 3.0; control2 = (start - end) / 3.0; } - auto const height = [&](glm::dvec3 const &Point) { return Origin.y + Grade * glm::dot(glm::dvec2{Point.x - Origin.x, Point.z - Origin.z}, Direction); }; + 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); @@ -1614,6 +1520,11 @@ double editor_track::sampled_grade(std::vector const &Samples, dou 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()) @@ -1815,7 +1726,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) { @@ -1831,8 +1742,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()) { @@ -1840,9 +1750,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); } @@ -2045,7 +1953,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 @@ -2085,7 +1993,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}; @@ -2242,20 +2150,22 @@ TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) return track; } -double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Point) +glm::dvec3 editor_track::point_at(TTrack const &Track, double const T) { - 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 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 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)) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 34b0a7d8..cc6d106c 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 { @@ -177,6 +180,7 @@ class editor_track 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 @@ -211,6 +215,8 @@ 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); From 7b8d809078edfa718301e0eefab8f3d7a56a1d5e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:35:40 +0200 Subject: [PATCH 05/34] refactor(editor): drop unused editor state and vertical leftovers of the route design --- application/editormode.h | 2 -- application/editormodeinfra.cpp | 14 ++------------ application/editormodetrack.cpp | 12 +----------- editor/editorAlignment.cpp | 6 ++---- editor/editorAlignment.hpp | 4 ---- editor/editorTrack.cpp | 6 ------ editor/editorTrack.hpp | 1 - 7 files changed, 5 insertions(+), 40 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 082682e4..32a6adaa 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -472,7 +472,6 @@ class editor_mode : public application_mode 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(); @@ -575,7 +574,6 @@ class editor_mode : public application_mode std::vector m_bindings; std::vector> m_bindings_unresolved; // lines kept as they are bool m_bindings_loaded{false}; - bool m_bindings_changed{false}; std::vector m_infra_buffer; // states of the objects before the change in progress bool m_infra_suspended{false}; void infra_hooks(); diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index d81dd2f2..e950c5d7 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -374,11 +374,6 @@ bool nearest_beside(Areas_ const &Areas, glm::dvec3 const &Point, double const C return best >= 0.0; } -bool station_near(std::vector const &Areas, glm::dvec3 const &Point, double const Corridor, infra::station &Station) -{ - return nearest_beside(Areas, Point, Corridor, Station); -} - // paths of the whole scenery by cells of the plan, for the lookups reaching up to kReach from the axis class track_index { @@ -662,7 +657,6 @@ void editor_mode::infra_store() 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)); } - m_bindings_changed = false; } std::vector editor_mode::infra_scope() const @@ -830,8 +824,6 @@ void editor_mode::infra_bind_chosen() candidate.chosen = false; ++count; } - if (count > 0) - m_bindings_changed = true; 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); } @@ -848,8 +840,6 @@ void editor_mode::infra_unbind(std::vector Indices) candidate.bound = false; m_bindings.erase(m_bindings.begin() + *index); } - if (false == Indices.empty()) - m_bindings_changed = true; } infra::object_state editor_mode::infra_state_of(infra::binding const &Binding) const @@ -973,7 +963,7 @@ void editor_mode::infra_follow(std::vector const &Tracks) // 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 (station_near(areas, Foot, 30.0, Station)) + if (nearest_beside(areas, Foot, 30.0, Station)) return true; if (false == everywhere.has_value()) { @@ -983,7 +973,7 @@ void editor_mode::infra_follow(std::vector const &Tracks) everywhere->push_back(area_of(*track)); } // only a path running where the old one did, not the one alongside - return station_near(*everywhere, Foot, 1.0, Station); + return nearest_beside(*everywhere, Foot, 1.0, Station); }; for (auto &binding : m_bindings) { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index ca5f7722..8d0a2cdf 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -93,12 +93,10 @@ 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; } } @@ -803,7 +801,6 @@ 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() @@ -1013,13 +1010,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}; } @@ -2361,7 +2352,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; } diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index bdd06cf3..29a85b01 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -245,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; @@ -712,12 +711,11 @@ result fit_between(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}); diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index a3e4572f..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}; @@ -83,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); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index c40c2dcd..7d815d66 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -2261,12 +2261,6 @@ void editor_track::retire(TTrack &Track) on_retire(Track); } -void editor_track::revive(TTrack &Track) -{ - Track.m_editorremoved = false; - commit({&Track}); -} - void editor_track::rebuild_geometry(TTrack &Track) { auto §ion{simulation::Region->section(Track.location())}; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index cc6d106c..4a80dc95 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -222,7 +222,6 @@ class editor_track 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); From c85a52d5b1aceedaa071c210cc87aef495c24443 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:37:26 +0200 Subject: [PATCH 06/34] refactor(editor): track observer instead of the function hooks --- application/editormode.cpp | 7 ++++++- application/editormode.h | 12 +++++++----- application/editormodeinfra.cpp | 19 ++++++++----------- editor/editorTrack.cpp | 21 +++++++++------------ editor/editorTrack.hpp | 19 +++++++++++++------ 5 files changed, 43 insertions(+), 35 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index c4cedb68..08f5103c 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -344,9 +344,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); }); - infra_hooks(); + editor_track::observe(this); } +editor_mode::~editor_mode() +{ + editor_track::observe(nullptr); +} + editor_ui *editor_mode::ui() const { return static_cast(m_userinterface.get()); diff --git a/application/editormode.h b/application/editormode.h index 32a6adaa..d8529da0 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -34,12 +34,13 @@ 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: // constructors editor_mode(); + ~editor_mode() override; // methods // initializes internal data structures of the mode. returns: true on success, false otherwise bool init() override; @@ -576,7 +577,6 @@ class editor_mode : public application_mode 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_hooks(); void infra_load(); void infra_store(); std::vector infra_scope() const; @@ -587,9 +587,11 @@ class editor_mode : public application_mode 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); - void infra_capture(TTrack const &Track); - void infra_follow(std::vector const &Tracks); - void infra_split(TTrack &Original, TTrack &Created, segment_data const &First); + // 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(); diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index e950c5d7..090bc3ef 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -461,14 +461,6 @@ double turn_of(infra::binding const &Binding, double const Yaw) } // namespace -void editor_mode::infra_hooks() -{ - editor_track::on_capture = [this](TTrack const &Track) { infra_capture(Track); }; - editor_track::on_commit = [this](std::vector const &Tracks) { infra_follow(Tracks); }; - editor_track::on_retire = [this](TTrack &) { infra_follow({}); }; - editor_track::on_split = [this](TTrack &Original, TTrack &Created, segment_data const &First) { infra_split(Original, Created, First); }; -} - // 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 @@ -908,7 +900,7 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh m_infra_buffer.push_back(infra_state_of(Binding)); } -void editor_mode::infra_capture(TTrack const &Track) +void editor_mode::track_captured(TTrack const &Track) { if (m_infra_suspended || false == m_infra.follow) return; @@ -948,7 +940,7 @@ void editor_mode::infra_capture(TTrack const &Track) } } -void editor_mode::infra_follow(std::vector const &Tracks) +void editor_mode::tracks_committed(std::vector const &Tracks) { if (m_infra_suspended || false == m_infra.follow) return; @@ -1027,7 +1019,12 @@ void editor_mode::infra_follow(std::vector const &Tracks) } } -void editor_mode::infra_split(TTrack &Original, TTrack &Created, segment_data const &First) +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; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 7d815d66..e20fe976 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -122,15 +122,12 @@ bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool } -std::function editor_track::on_capture; -std::function const &)> editor_track::on_commit; -std::function editor_track::on_retire; -std::function editor_track::on_split; +editor_track::observer *editor_track::s_observer{nullptr}; editor_track::state editor_track::capture(TTrack const &Track) { - if (on_capture) - on_capture(Track); + if (s_observer != nullptr) + s_observer->track_captured(Track); state result; result.paths = Track.m_paths; result.velocity = Track.fVelocity; @@ -240,8 +237,8 @@ void editor_track::commit(std::vector const &Tracks) for (auto *track : affected) for (auto *vehicle : track->Dynamics) vehicle->Move(0.000001); - if (on_commit) - on_commit(Tracks); + if (s_observer != nullptr) + s_observer->tracks_committed(Tracks); } void editor_track::commit_parameters(TTrack &Track) @@ -2219,8 +2216,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 (on_split) - on_split(Track, *created, first); + if (s_observer != nullptr) + s_observer->track_split(Track, *created, first); commit({&Track, created}); return created; } @@ -2257,8 +2254,8 @@ void editor_track::retire(TTrack &Track) update_transition(*neighbour); rebuild_geometry(*neighbour); } - if (on_retire) - on_retire(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 4a80dc95..742e393d 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -232,14 +232,21 @@ class editor_track static void velocity(TTrack &Track, double const Velocity); static void damage(TTrack &Track, int const Damage); - // let the objects which belong to the paths follow their changes - static std::function on_capture; - static std::function const &)> on_commit; - static std::function on_retire; - // called before the commit; the first argument keeps the beginning of the path, given in the last one - static std::function on_split; + // 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); From 62f07c8fbb56288cc7562850f5654e9490d498cf Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:39:30 +0200 Subject: [PATCH 07/34] refactor(editor): one edit helper for the definitions rewritten on save --- scene/scenelayerssave.cpp | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp index 02029a3a..bb77fae0 100644 --- a/scene/scenelayerssave.cpp +++ b/scene/scenelayerssave.cpp @@ -781,6 +781,15 @@ 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) { @@ -811,13 +820,7 @@ save_result node_layers::save(std::vector const &Rootstatements) state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; return false; } - file_patch::edit edit; - edit.begin = source.span.begin; - edit.end = source.span.end; - edit.length = text.size(); - edit.text = std::move(text); - edit.node = Node; - state.patches[source.layer].edits.emplace_back(std::move(edit)); + push_edit(source.layer, source.span, std::move(text), Node); rewritten.emplace(Node); patchednodes.push_back(Node); return true; @@ -884,21 +887,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); } for (auto const &erased : m_erased) @@ -919,14 +917,10 @@ save_result node_layers::save(std::vector const &Rootstatements) { for (auto const &line : lookup->second) { - if (line.mark != mark) + if (line.mark == mark) { - continue; + push_edit(markedlayer, line.span, {}, nullptr); } - file_patch::edit edit; - edit.begin = line.span.begin; - edit.end = line.span.end; - state.patches[markedlayer].edits.emplace_back(std::move(edit)); } } auto const target{resolve(markedlayer)}; From 9cd0f0b1a79ddff28bc2ae06e3bcc32f92f70049 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:39:30 +0200 Subject: [PATCH 08/34] refactor(editor): shared writers of the path definitions --- editor/editorTrack.cpp | 87 +++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index e20fe976..e328cbb3 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1831,6 +1831,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) @@ -1842,31 +1880,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); @@ -2120,28 +2141,14 @@ 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; From 20fa5613a78cf2bdcac87ca751b1e634cda44c2d Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:42:47 +0200 Subject: [PATCH 09/34] refactor(editor): shared gizmo frame, screen picking and history trimming --- application/editormode.h | 3 +- application/editormodeprofile.cpp | 15 ++-- application/editormodetrack.cpp | 143 +++++++++--------------------- application/editorprojection.h | 39 ++++++++ 4 files changed, 89 insertions(+), 111 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index d8529da0..d099334d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -289,7 +289,8 @@ class editor_mode : public application_mode, private editor_track::observer 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; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 179ce97f..84eef793 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -869,6 +869,11 @@ void editor_mode::render_profile_canvas() } auto const curves{profile::curves(line)}; + auto const mouse_distance2 = [&](double Chainage, double Elevation) { + auto const dx{x_of(Chainage) - mouse.x}; + auto const dy{y_of(Elevation) - mouse.y}; + return dx * dx + dy * dy; + }; auto const hit_point = [&]() { int hit{-1}; float best{64.0f}; @@ -876,11 +881,9 @@ void editor_mode::render_profile_canvas() { if (points[i].joint) continue; - auto const dx{x_of(points[i].chainage) - mouse.x}; - auto const dy{y_of(points[i].elevation) - mouse.y}; - if (dx * dx + dy * dy < best) + if (auto const distance{mouse_distance2(points[i].chainage, points[i].elevation)}; distance < best) { - best = dx * dx + dy * dy; + best = distance; hit = i; } } @@ -892,9 +895,7 @@ void editor_mode::render_profile_canvas() if (points[c.point].joint) continue; auto const chainage{points[c.point].chainage}; - auto const dx{x_of(chainage) - mouse.x}; - auto const dy{y_of(profile::elevation(line, chainage)) - mouse.y}; - if (dx * dx + dy * dy < 64.0f) + if (mouse_distance2(chainage, profile::elevation(line, chainage)) < 64.0f) return static_cast(c.point); } return -1; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 8d0a2cdf..bfe42d6f 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -108,7 +108,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) @@ -116,12 +115,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) * (editor_track::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; @@ -208,28 +202,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()) { @@ -269,7 +253,7 @@ 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); @@ -318,15 +302,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(); @@ -336,6 +324,7 @@ void editor_mode::push_track_snapshot(std::vector(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; } @@ -1135,21 +1113,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()) { @@ -1161,7 +1130,7 @@ 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]}; @@ -1812,19 +1781,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; } } @@ -1897,22 +1860,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()) { @@ -1944,7 +1898,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) { @@ -2250,21 +2204,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) @@ -2365,8 +2312,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); @@ -2422,7 +2368,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]}; @@ -2432,10 +2377,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]}; @@ -2576,8 +2518,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; } @@ -2692,8 +2633,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(); } @@ -3408,8 +3348,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; } @@ -3548,8 +3487,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; } @@ -3729,7 +3667,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 index aa5cd45d..f867b62d 100644 --- a/application/editorprojection.h +++ b/application/editorprojection.h @@ -11,7 +11,11 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "rendering/renderer.h" +#include "utilities/Globals.h" #include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" + +#include // world to screen projection of the current camera, for overlays drawn with ImGui class screen_projection @@ -54,6 +58,15 @@ class screen_projection 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}; @@ -61,3 +74,29 @@ class screen_projection 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); + } +}; From 0879f1528e33821f1b4da8be41be79eeaf61a3c0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:43:41 +0200 Subject: [PATCH 10/34] refactor(editor): named overlay colours --- application/editormodeinfra.cpp | 2 +- application/editormodeprofile.cpp | 16 +++++----- application/editormodetrack.cpp | 52 +++++++++++++++---------------- application/editorprojection.h | 9 ++++++ 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 090bc3ef..6630b8d9 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -1358,7 +1358,7 @@ void editor_mode::draw_infra_overlay() const drawlist->AddCircleFilled(screen, Size, Colour); }; for (auto const &binding : m_bindings) - draw(binding, binding.lost ? IM_COL32(255, 60, 60, 255) : IM_COL32(90, 230, 110, 220), 3.5f); + 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]}; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 84eef793..e4dca762 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -1060,10 +1060,10 @@ void editor_mode::render_profile_canvas() } auto const chainage{points[c.point].chainage}; ImVec2 const grip{x_of(chainage), y_of(profile::elevation(line, chainage))}; - draw->AddCircleFilled(grip, 4.5f, IM_COL32(60, 230, 90, 255), 4); + draw->AddCircleFilled(grip, 4.5f, overlay_color::grip, 4); if (static_cast(c.point) == state.curve_grip) - draw->AddCircle(grip, 9.0f, IM_COL32(255, 255, 255, 255), 12, 2.0f); - draw->AddText(ImVec2(grip.x + 7.0f, grip.y + 4.0f), IM_COL32(60, 230, 90, 255), format("R %.0f", points[c.point].radius).c_str()); + 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) { @@ -1073,10 +1073,10 @@ void editor_mode::render_profile_canvas() 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, IM_COL32(255, 210, 60, 255)); + draw->AddCircleFilled(at, 5.5f, overlay_color::marked); if (i == state.selected) - draw->AddCircle(at, 10.0f, IM_COL32(255, 255, 255, 255), 16, 2.0f); - draw->AddText(ImVec2(at.x + 7.0f, at.y - 18.0f), IM_COL32(255, 210, 60, 255), format("%d %.3f", i + 1, points[i].elevation).c_str()); + 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) { @@ -1157,8 +1157,8 @@ void editor_mode::draw_profile_overlay() const ImVec2 screen; if (false == projection.project(position, screen)) continue; - drawlist->AddCircleFilled(screen, i == state.selected ? 7.0f : 5.0f, IM_COL32(255, 210, 60, 255)); - drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 16.0f), IM_COL32(255, 210, 60, 255), format("%d", i + 1).c_str()); + 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) { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index bfe42d6f..1d1be9f5 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1056,7 +1056,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) @@ -1067,7 +1067,7 @@ 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); } } @@ -1086,19 +1086,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); + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; 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), IM_COL32(60, 230, 90, 255), radius.c_str()); + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str()); } - drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), IM_COL32(255, 210, 60, 255), format("W%d", i + 1).c_str()); + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); } } @@ -1423,20 +1423,20 @@ 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{plan_of(state.tool_handle - state.tool_point)}; @@ -1444,7 +1444,7 @@ void editor_mode::draw_straights_overlay() const { 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 @@ -1456,7 +1456,7 @@ void editor_mode::draw_straights_overlay() const 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) @@ -1469,7 +1469,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; } } @@ -1481,7 +1481,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); } } } @@ -1495,11 +1495,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}; @@ -2788,33 +2788,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()}; diff --git a/application/editorprojection.h b/application/editorprojection.h index f867b62d..31825b0b 100644 --- a/application/editorprojection.h +++ b/application/editorprojection.h @@ -17,6 +17,15 @@ http://mozilla.org/MPL/2.0/. #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 { From 1f381bbb48172134729fa7e1759982c9cd9cfd4c Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:45:54 +0200 Subject: [PATCH 11/34] refactor(editor): split the profile canvas into navigation, editing and drawing --- application/editormode.h | 6 + application/editormodeprofile.cpp | 317 +++++++++++++++++------------- 2 files changed, 189 insertions(+), 134 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index d099334d..25a2f0a7 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -543,7 +543,13 @@ class editor_mode : public application_mode, private editor_track::observer 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; diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index e4dca762..e094a886 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -796,84 +796,130 @@ void editor_mode::render_profile_point() 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 &line{state.line}; - auto &points{line.points}; - auto const &samples{state.samples}; - ImGuiIO const &io{ImGui::GetIO()}; - 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()}; - ImDrawList *draw{ImGui::GetWindowDrawList()}; - float const strip{state.show_plan ? 44.0f : 0.0f}; - float const top{18.0f}; - float const height{size.y - strip - top}; - auto const width{std::max(1.0, state.view_to - state.view_from)}; - auto const across{width / size.x}; if (state.exaggeration <= 0.0f) - { - double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; - for (auto const &sample : samples) - { - low = std::min(low, sample.position.y); - high = std::max(high, sample.position.y); - } - for (auto const &point : 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(across / (range / height), 1.0, 500.0)); - state.view_centre = (low + high) * 0.5; - } - auto const vertical{across / state.exaggeration}; - auto const x_of = [&](double const Chainage) { return corner.x + static_cast((Chainage - state.view_from) / across); }; - auto const y_of = [&](double const Height) { return corner.y + top + height * 0.5f - static_cast((Height - state.view_centre) / vertical); }; - auto const chainage_of = [&](float const X) { return state.view_from + (X - corner.x) * across; }; - auto const height_of = [&](float const Y) { return state.view_centre - (Y - corner.y - top - height * 0.5f) * vertical; }; - ImVec2 const mouse{io.MousePos}; + 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; - if (hovered && io.MouseWheel != 0.0f) + 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{chainage_of(mouse.x)}; + 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))) + 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 * across; - state.view_to -= io.MouseDelta.x * across; - state.view_centre += io.MouseDelta.y * vertical; + 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 curves{profile::curves(line)}; - auto const mouse_distance2 = [&](double Chainage, double Elevation) { - auto const dx{x_of(Chainage) - mouse.x}; - auto const dy{y_of(Elevation) - mouse.y}; - return dx * dx + dy * dy; - }; auto const hit_point = [&]() { int hit{-1}; float best{64.0f}; @@ -881,7 +927,7 @@ void editor_mode::render_profile_canvas() { if (points[i].joint) continue; - if (auto const distance{mouse_distance2(points[i].chainage, points[i].elevation)}; distance < best) + if (auto const distance{View.mouse_distance2(points[i].chainage, points[i].elevation)}; distance < best) { best = distance; hit = i; @@ -890,18 +936,18 @@ void editor_mode::render_profile_canvas() return hit; }; auto const hit_grip = [&]() { - for (auto const &c : curves) + for (auto const &c : profile::curves(line)) { if (points[c.point].joint) continue; auto const chainage{points[c.point].chainage}; - if (mouse_distance2(chainage, profile::elevation(line, chainage)) < 64.0f) + 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)) + if (Hovered && ImGui::IsMouseClicked(0)) { auto const point{hit_point()}; auto const grip{point < 0 ? hit_grip() : -1}; @@ -912,10 +958,10 @@ void editor_mode::render_profile_canvas() else state.selected = -1; } - if (hovered && ImGui::IsMouseDoubleClicked(0) && state.dragging < 0 && state.curve_grip < 0 && points.size() >= 2) + if (Hovered && ImGui::IsMouseDoubleClicked(0) && state.dragging < 0 && state.curve_grip < 0 && points.size() >= 2) { - auto const chainage{chainage_of(mouse.x)}; - if (chainage > points.front().chainage + 0.5 && chainage < points.back().chainage - 0.5 && std::abs(y_of(profile::elevation(line, chainage)) - mouse.y) < 12.0f) + 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) @@ -937,13 +983,13 @@ void editor_mode::render_profile_canvas() 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(chainage_of(mouse.x) * 100.0) / 100.0, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + 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(height_of(mouse.y) * 1000.0) / 1000.0; + point.elevation = std::round(View.height_of(mouse.y) * 1000.0) / 1000.0; changed = true; } else @@ -955,7 +1001,7 @@ void editor_mode::render_profile_canvas() { if (ImGui::IsMouseDown(0)) { - auto const sagitta{height_of(mouse.y) - points[state.curve_grip].elevation}; + 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; @@ -965,39 +1011,43 @@ void editor_mode::render_profile_canvas() } 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))) + 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; } - if (changed) - profile_edited(); - state.hover = hovered ? chainage_of(mouse.x) : -1.0; + return changed; +} - ImVec2 const bottomright{corner.x + size.x, corner.y + size.y}; - draw->AddRectFilled(corner, bottomright, IM_COL32(22, 24, 28, 255)); - draw->PushClipRect(corner, bottomright, true); - auto const plotbottom{corner.y + top + height}; +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(across * 110.0)}; + 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{x_of(s)}; - draw->AddLine(ImVec2(x, corner.y + top), ImVec2(x, plotbottom), kGrid); - draw->AddText(ImVec2(x + 2.0f, corner.y + 2.0f), kLabel, profile::format_chainage(s + state.origin).c_str()); + 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(vertical * 45.0)}; - for (auto h = std::floor(height_of(plotbottom) / heightstep) * heightstep; h <= height_of(corner.y + top); h += heightstep) + 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{y_of(h)}; - draw->AddLine(ImVec2(corner.x, y), ImVec2(bottomright.x, y), kGrid); - draw->AddText(ImVec2(corner.x + 2.0f, y - 14.0f), kLabel, format("%.1f", h).c_str()); + 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(x_of(zone.from), corner.y + top), ImVec2(std::max(x_of(zone.to), x_of(zone.from) + 2.0f), plotbottom), kSwitch); - draw->AddText(ImVec2(x_of(zone.from) + 2.0f, corner.y + top + 2.0f), IM_COL32(200, 160, 255, 220), zone.name.c_str()); + 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) { @@ -1006,18 +1056,18 @@ void editor_mode::render_profile_canvas() float lastx{-1e9f}; for (std::size_t i = 0; i < samples.size(); ++i) { - auto const x{x_of(samples[i].chainage)}; + auto const x{View.x_of(samples[i].chainage)}; auto const h{Height(i)}; - if (std::isnan(h) || x < corner.x - 50.0f || x > bottomright.x + 50.0f) + 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, y_of(h)}; + ImVec2 const next{x, View.y_of(h)}; if (open) - draw->AddLine(previous, next, Colour, Thickness); + Draw.AddLine(previous, next, Colour, Thickness); previous = next; lastx = x; open = true; @@ -1029,102 +1079,101 @@ void editor_mode::render_profile_canvas() 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(x_of(points[i].chainage), y_of(points[i].elevation)), ImVec2(x_of(points[i + 1].chainage), y_of(points[i + 1].elevation)), kTangents, 1.0f); + 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, chainage_of(corner.x))}; - auto const to{std::min(points.back().chainage, chainage_of(bottomright.x))}; - ImVec2 previous{x_of(from), y_of(profile::elevation(line, from))}; - for (auto x = previous.x + 2.0f; x <= x_of(to) + 2.0f; x += 2.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, chainage_of(x))}; - ImVec2 const next{x_of(chainage), y_of(profile::elevation(line, chainage))}; - draw->AddLine(previous, next, kGradeLine, 2.5f); + 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{x_of(middle)}; - if (x_of(points[i + 1].chainage) - x_of(points[i].chainage) < 70.0f) + 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, y_of(profile::elevation(line, middle)) - 30.0f), kGradeLine, text.c_str()); + Draw.AddText(ImVec2(x - 40.0f, View.y_of(profile::elevation(line, middle)) - 30.0f), kGradeLine, text.c_str()); } - for (auto const &c : curves) + for (auto const &c : profile::curves(line)) { for (auto const at : {c.start, c.end}) { - auto const y{y_of(profile::elevation(line, at))}; - draw->AddLine(ImVec2(x_of(at), y - 7.0f), ImVec2(x_of(at), y + 7.0f), kGradeLine, 1.5f); + 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{x_of(chainage), y_of(profile::elevation(line, chainage))}; - draw->AddCircleFilled(grip, 4.5f, overlay_color::grip, 4); + 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()); + 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{x_of(points[i].chainage), y_of(points[i].elevation)}; + 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)); + 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)); + 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); + 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()); + 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{x_of(issue.chainage)}; - auto const colour{issue.error ? kError : kWarning}; - draw->AddTriangleFilled(ImVec2(x - 5.0f, corner.y + top), ImVec2(x + 5.0f, corner.y + top), ImVec2(x, corner.y + top + 8.0f), colour); + 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 + strip * 0.5f}; - draw->AddLine(ImVec2(corner.x, plotbottom), ImVec2(bottomright.x, plotbottom), IM_COL32(255, 255, 255, 60)); - draw->AddLine(ImVec2(corner.x, middle), ImVec2(bottomright.x, middle), IM_COL32(255, 255, 255, 40)); + 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{x_of(samples[i].chainage)}; - if (x < corner.x - 50.0f || x > bottomright.x + 50.0f || (open && x - lastx < 1.0f)) + 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 * strip * 0.4), -strip * 0.45f, strip * 0.45f)}; + 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); + Draw.AddLine(previous, next, IM_COL32(120, 190, 255, 255), 1.5f); previous = next; lastx = x; open = true; } - draw->AddText(ImVec2(corner.x + 2.0f, plotbottom + 1.0f), kLabel, "plan: curvature (up: left)"); + Draw.AddText(ImVec2(View.corner.x + 2.0f, plotbottom + 1.0f), kLabel, "plan: curvature (up: left)"); } - if (hovered && state.hover >= 0.0 && state.hover <= state.route.length && false == samples.empty()) - { - auto const x{x_of(state.hover)}; - draw->AddLine(ImVec2(x, corner.y + top), ImVec2(x, bottomright.y), IM_COL32(255, 255, 255, 90)); - auto const designed{profile::elevation(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(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(); - } - draw->PopClipRect(); +} + +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 From c95c37408252adb1b31214f7f2eaa177c82bc2fe Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:47:00 +0200 Subject: [PATCH 12/34] refactor(editor): split the route design panel --- application/editormode.h | 5 + application/editormodetrack.cpp | 183 +++++++++++++++++++------------- 2 files changed, 114 insertions(+), 74 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 25a2f0a7..6a868119 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -316,6 +316,11 @@ class editor_mode : public application_mode, private editor_track::observer 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); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1d1be9f5..67d78db9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1163,12 +1163,34 @@ void editor_mode::render_route_gizmo() 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()); @@ -1191,17 +1213,11 @@ 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"); @@ -1236,7 +1252,14 @@ 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")) { @@ -1281,70 +1304,83 @@ 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); - if (ImGui::Selectable(format("Vertex W%d", i + 1).c_str(), 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); - 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; - } - - 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); + 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); + 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; + } + + 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) { @@ -1381,8 +1417,7 @@ void editor_mode::render_route_ui() { ImGui::SameLine(); ImGui::TextUnformatted(route.status.c_str()); - } -} + }} editor_track::straight const &editor_mode::current_straight() { From eb5b2e891914973d0ee686770562cd456d4fce52 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:48:00 +0200 Subject: [PATCH 13/34] refactor(editor): split the infrastructure window --- application/editormode.h | 4 ++ application/editormodeinfra.cpp | 99 +++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 6a868119..4207a117 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -607,6 +607,10 @@ class editor_mode : public application_mode, private editor_track::observer 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}; diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 6630b8d9..c099bdb8 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -1137,31 +1137,7 @@ void editor_mode::render_infra_window() { state.follow = follow; if (follow) - { - // what changed in the meantime becomes the new starting point - 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; - } - double s; - bool interior; - infra::project(anchor.at.track->m_paths[anchor.at.path], points[i], s, interior); - anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, points[i]); - } - if (binding.turns && yaw.has_value()) - binding.yaw = turn_of(binding, *yaw); - } - } + 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" @@ -1176,6 +1152,46 @@ void editor_mode::render_infra_window() } 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; + } + double s; + bool interior; + infra::project(anchor.at.track->m_paths[anchor.at.path], points[i], s, interior); + anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, 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)); @@ -1209,7 +1225,17 @@ void editor_mode::render_infra_window() 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()) { @@ -1221,12 +1247,10 @@ void editor_mode::render_infra_window() infra_bind_chosen(); ImGui::SameLine(); if (ImGui::SmallButton("All")) - for (auto &candidate : candidates) - candidate.chosen = (false == candidate.bound); + choose({}, true); ImGui::SameLine(); if (ImGui::SmallButton("None")) - for (auto &candidate : candidates) - candidate.chosen = false; + choose({}, false); ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); for (auto const group : infra::categories()) @@ -1239,14 +1263,10 @@ void editor_mode::render_infra_window() ImGui::PopStyleColor(); ImGui::SameLine(); if (ImGui::SmallButton((std::string{"all##g"} + infra::name(group)).c_str())) - for (auto &candidate : candidates) - if (candidate.binding.group == group) - candidate.chosen = (false == candidate.bound); + choose(group, true); ImGui::SameLine(); if (ImGui::SmallButton((std::string{"none##g"} + infra::name(group)).c_str())) - for (auto &candidate : candidates) - if (candidate.binding.group == group) - candidate.chosen = false; + choose(group, false); if (false == open) continue; // the candidates are sorted by the category @@ -1282,7 +1302,10 @@ void editor_mode::render_infra_window() } ImGui::EndChild(); } +} +void editor_mode::render_infra_bound() +{ if (ImGui::TreeNode("Bound objects along the scope")) { auto const scope{infra_scope()}; @@ -1323,12 +1346,6 @@ void editor_mode::render_infra_window() infra_unbind({*unbind}); ImGui::TreePop(); } - - 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(); } void editor_mode::draw_infra_overlay() const From 21082c4a5f658d22fa27809b9707166b323ba6f2 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:48:42 +0200 Subject: [PATCH 14/34] refactor(editor): split the vertical profile window --- application/editormode.h | 3 ++ application/editormodeprofile.cpp | 84 +++++++++++++++++++------------ 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 4207a117..28b56aa9 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -540,6 +540,9 @@ class editor_mode : public application_mode, private editor_track::observer 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_resample(); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index e094a886..98f45a96 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -537,6 +537,48 @@ void editor_mode::render_profile_window() 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); @@ -566,31 +608,11 @@ void editor_mode::render_profile_window() } ImGui::TreePop(); } - 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"); - } +} +bool editor_mode::render_profile_parameters() +{ + auto &state{m_profile}; auto &line{state.line}; bool changed{false}; ImGui::PushItemWidth(90.0f); @@ -648,14 +670,12 @@ void editor_mode::render_profile_window() 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"); - if (changed) - profile_edited(); + return changed; +} - render_profile_canvas(); - - ImGui::Columns(2, "##profilecolumns", true); - render_profile_point(); - ImGui::NextColumn(); +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()) @@ -681,8 +701,6 @@ void editor_mode::render_profile_window() if (false == state.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); ImGui::EndChild(); - ImGui::Columns(1); - ImGui::End(); } void editor_mode::render_profile_point() From 07994a578c32e87f974898ff0bbe47fa0170bd09 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:50:08 +0200 Subject: [PATCH 15/34] refactor(editor): stored bindings read apart from their registration, one way to open a profile route --- application/editormode.h | 1 + application/editormodeinfra.cpp | 238 ++++++++++++++++-------------- application/editormodeprofile.cpp | 23 +-- 3 files changed, 137 insertions(+), 125 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 28b56aa9..c87dbefc 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -545,6 +545,7 @@ class editor_mode : public application_mode, private editor_track::observer 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(); diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index c099bdb8..2e976d9d 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -459,12 +459,130 @@ double turn_of(infra::binding const &Binding, double const Yaw) return clamp_circular(Yaw - infra::heading(axis.forward)); } -} // namespace +// 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)]}; + double s; + bool interior; + infra::project((*track)->m_paths[path], point, s, interior); + binding.anchors.push_back(infra::make_anchor({*track, path, s}, 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()) @@ -474,35 +592,7 @@ void editor_mode::infra_load() if (marked.empty()) return; - point_grid paths; - 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); - point_grid models; - for (auto *model : simulation::Instances.sequence()) - if (model != nullptr && false == model->from_template() && model->Model() != nullptr) - models.add(model->location(), model); - point_grid traction; - for (auto *piece : simulation::Traction.sequence()) - if (piece != nullptr && false == piece->from_template()) - traction.add(piece->pPoint1, piece); - point_grid logic; - 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); - point_grid includes; - 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); - } - - double const tolerance{0.05}; + bindable_objects const objects; auto resolved{0}; for (auto const layer : marked) { @@ -511,93 +601,13 @@ void editor_mode::infra_load() { if (text.empty() || text.front() == '#') continue; - 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; - auto const keep = [&]() { m_bindings_unresolved.emplace_back(layer, text); }; - if (line.fail() || count < 1 || count > 4) + auto binding{read_binding(text, context, objects)}; + if (false == binding.has_value()) { - keep(); + m_bindings_unresolved.emplace_back(layer, text); continue; } - 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{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{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{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{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)) - { - keep(); - continue; - } - 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{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)]}; - double s; - bool interior; - infra::project((*track)->m_paths[path], point, s, interior); - binding.anchors.push_back(infra::make_anchor({*track, path, s}, point)); - } - if (false == found || binding.anchors.size() != points.size()) - { - keep(); - continue; - } - binding.turns = binding.turns && yaw.has_value(); - if (binding.turns) - binding.yaw = turn_of(binding, *yaw); - binding.label = object_label(binding); - m_bindings.push_back(std::move(binding)); + m_bindings.push_back(std::move(*binding)); ++resolved; } } diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 98f45a96..43a57374 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -347,14 +347,7 @@ void editor_mode::profile_open(TTrack *From, TTrack *To) editor_track::route route; if (false == editor_track::find_route(From, To, route, state.error)) return; - 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(); + profile_take(std::move(route), From, To); } void editor_mode::profile_open_run(TTrack &Track) @@ -369,9 +362,17 @@ void editor_mode::profile_open_run(TTrack &Track) state.error = "The path can't carry a profile"; return; } - state.route = std::move(route); - state.from = state.picked_from = state.route.spans.front().track; - state.to = state.picked_to = state.route.spans.back().track; + 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()) From 7d87fef7a05e8c3659b02a91fad656c90e5ca454 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:51:05 +0200 Subject: [PATCH 16/34] refactor(editor): anchor on the nearest place of a path --- application/editormodeinfra.cpp | 15 +++------------ editor/editorInfra.cpp | 8 ++++++++ editor/editorInfra.hpp | 2 ++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 2e976d9d..67cf2333 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -567,10 +567,7 @@ std::optional read_binding(std::string const &Text, scene::layer break; } auto const &point{points[std::min(i, points.size() - 1)]}; - double s; - bool interior; - infra::project((*track)->m_paths[path], point, s, interior); - binding.anchors.push_back(infra::make_anchor({*track, path, s}, point)); + binding.anchors.push_back(infra::make_anchor(*track, path, point)); } if (false == found || binding.anchors.size() != points.size()) return std::nullopt; @@ -938,10 +935,7 @@ void editor_mode::track_captured(TTrack const &Track) auto placed{anchor}; if (glm::distance(infra::place(placed), points[i]) <= 0.01) continue; - double s; - bool interior; - infra::project(path, points[i], s, interior); - anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, points[i]); + 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); @@ -1189,10 +1183,7 @@ void editor_mode::infra_rebase() binding.lost = true; continue; } - double s; - bool interior; - infra::project(anchor.at.track->m_paths[anchor.at.path], points[i], s, interior); - anchor = infra::make_anchor({anchor.at.track, anchor.at.path, s}, points[i]); + anchor = infra::make_anchor(anchor.at.track, anchor.at.path, points[i]); } if (binding.turns && yaw.has_value()) binding.yaw = turn_of(binding, *yaw); diff --git a/editor/editorInfra.cpp b/editor/editorInfra.cpp index a2fbc9ba..ba38961f 100644 --- a/editor/editorInfra.cpp +++ b/editor/editorInfra.cpp @@ -222,6 +222,14 @@ 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; diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp index afb76d8e..64d32b25 100644 --- a/editor/editorInfra.hpp +++ b/editor/editorInfra.hpp @@ -83,6 +83,8 @@ struct anchor 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); From fc195880e1730ccb8e079b7efd490508f98df24e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:52:14 +0200 Subject: [PATCH 17/34] refactor(editor): one place for the orthographic projection of the editor view --- rendering/opengl33renderer.cpp | 27 +++++++++++++-------------- rendering/opengl33renderer.h | 1 + 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 767618a1..682ca59e 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -1518,6 +1518,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 +1604,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 +1704,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: diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 43cd24b0..9d9bba8c 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -399,6 +399,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; From 17725b65274c8374ac6d40e425a30babb9f26aa4 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:01:49 +0200 Subject: [PATCH 18/34] feat(editor): mode bar of the track window, key hints and values of the change at the cursor --- application/editormode.h | 10 ++ application/editormodetrack.cpp | 279 ++++++++++++++++++++++++++++---- 2 files changed, 261 insertions(+), 28 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index c87dbefc..a6b4f0f8 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -303,6 +303,8 @@ class editor_mode : public application_mode, private editor_track::observer 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{}; + std::string m_track_mode_notice; struct route_design { TTrack *from{nullptr}; @@ -475,6 +477,7 @@ class editor_mode : public application_mode, private editor_track::observer 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(); @@ -483,6 +486,13 @@ class editor_mode : public application_mode, private editor_track::observer 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}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 67d78db9..67b1cf68 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -39,6 +39,7 @@ 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) @@ -81,7 +82,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) @@ -99,6 +103,10 @@ void editor_mode::select_track(scene::basic_node *Node) { m_track_tab = track_tab::route; } + else + { + m_track_tab = track_tab::path; + } } bool editor_mode::pick_track_handle() @@ -438,6 +446,7 @@ void editor_mode::render_track_window() ImGui::End(); return; } + render_track_modes(*selected_track()); switch (m_track_tab) { case track_tab::straights: @@ -457,12 +466,7 @@ void editor_mode::render_track_window() render_path_ui(); break; } - if (m_track_tab != track_tab::path) - { - ImGui::Separator(); - if (ImGui::SmallButton("Edit points and parameters of the single path")) - 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(); @@ -475,6 +479,54 @@ void editor_mode::render_track_window() 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()}; @@ -2359,32 +2411,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", turn) + (state.curve_radius > 0.0 ? format(" R %.0f m", state.curve_radius) : std::string{" R automatic"}); + } + 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() From 661c211b2e7f6ff8d1ae6cb2264321880eec7769 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:04:41 +0200 Subject: [PATCH 19/34] feat(editor): tooltips of the track fields, textures applied on leaving the field --- application/editormode.h | 2 +- application/editormodetrack.cpp | 64 ++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index a6b4f0f8..62f79ef4 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -303,7 +303,7 @@ class editor_mode : public application_mode, private editor_track::observer 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{}; + std::array m_track_material_edited{}; // path whose texture name is being typed std::string m_track_mode_notice; struct route_design { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 67b1cf68..4e3b9a94 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -62,6 +62,12 @@ editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, float const kHandleRadius{10.0f}; +void item_tooltip(char const *Text) +{ + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", Text); +} + } TTrack *editor_mode::selected_track() const @@ -547,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) @@ -583,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")) { @@ -603,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); } @@ -640,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"}; @@ -660,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(); @@ -676,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'; @@ -686,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(); } @@ -715,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) { @@ -767,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) { @@ -802,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")) @@ -818,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 { @@ -1273,6 +1307,7 @@ bool editor_mode::render_route_parameters() 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"}; @@ -1281,8 +1316,10 @@ bool editor_mode::render_route_parameters() 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(); @@ -1388,6 +1425,7 @@ bool editor_mode::render_route_vertex(int const Index) } 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"); @@ -1404,7 +1442,9 @@ bool editor_mode::render_route_vertex(int const Index) 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); @@ -1415,6 +1455,7 @@ bool editor_mode::render_route_vertex(int const Index) 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); @@ -1707,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")) { @@ -1829,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(); @@ -2245,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 @@ -2489,7 +2536,7 @@ std::string editor_mode::track_readout() const if (glm::length(offset) < 1e-3) return {}; auto const turn{glm::degrees(signed_angle(line.direction, glm::normalize(offset)))}; - return format("turn %+.3f deg", turn) + (state.curve_radius > 0.0 ? format(" R %.0f m", state.curve_radius) : std::string{" R automatic"}); + 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)); @@ -3120,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 From b0c363b586167bc101e7032b9ef617127ab2fc1d Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:27:41 +0200 Subject: [PATCH 20/34] feat: structure gauge along the line of the selected path in the OpenGL view --- CMakeLists.txt | 2 + application/editormode.cpp | 1 + application/editormode.h | 18 ++ application/editormodegauge.cpp | 402 ++++++++++++++++++++++++++++++++ application/editormodetrack.cpp | 1 + editor/editorGauge.cpp | 110 +++++++++ editor/editorGauge.hpp | 55 +++++ editor/editorTrack.cpp | 4 +- editor/editorTrack.hpp | 1 + rendering/editoroverlay.h | 42 ++++ rendering/opengl33renderer.cpp | 57 +++++ rendering/opengl33renderer.h | 4 + 12 files changed, 696 insertions(+), 1 deletion(-) create mode 100644 application/editormodegauge.cpp create mode 100644 editor/editorGauge.cpp create mode 100644 editor/editorGauge.hpp create mode 100644 rendering/editoroverlay.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 94d71aa6..04eb6763 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,12 +173,14 @@ set(SOURCES "editor/editorTrack.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/editormodeprofile.cpp" +"application/editormodegauge.cpp" "application/editormodeinfra.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 08f5103c..f074dc05 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1035,6 +1035,7 @@ bool editor_mode::update() draw_infra_overlay(); render_infra_window(); } + update_gauge(); // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) diff --git a/application/editormode.h b/application/editormode.h index 62f79ef4..7bdc8c2c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorAlignment.hpp" #include "editor/editorProfile.hpp" #include "editor/editorInfra.hpp" +#include "editor/editorGauge.hpp" #include #include @@ -305,6 +306,23 @@ class editor_mode : public application_mode, private editor_track::observer 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 enabled{false}; + std::vector profiles; + int profile{0}; + double reach{500.0}; // m, each way from the path + TTrack const *track{nullptr}; // scanned + std::size_t history{0}; + bool pending{false}; // rescan once the edit ends + std::size_t models{0}; + std::size_t intrusions{0}; // triangles + } m_gauge; + void gauge_load(); + void update_gauge(); + void scan_gauge(TTrack &Track); + void render_gauge_ui(); struct route_design { TTrack *from{nullptr}; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp new file mode 100644 index 00000000..2fbd9192 --- /dev/null +++ b/application/editormodegauge.cpp @@ -0,0 +1,402 @@ +/* +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/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 "world/Track.h" +#include "imgui/imgui.h" +#include +#include +#include +#include +#include + +using geometry::plan_of; + +namespace +{ + +char const *const gauge_file{"editor_gauges.txt"}; +std::size_t const gauge_triangle_limit{200000}; + +// samples of the route, found by the position in the plan +class corridor +{ +public: + struct local + { + std::size_t sample; + double lateral; // positive to the left + double height; // above the rail top + }; + + corridor(std::vector const &Samples, double const Margin) : m_samples{Samples}, m_margin{Margin} + { + m_min = m_max = plan_of(Samples.front().position); + for (auto const &sample : Samples) + { + m_min = glm::min(m_min, plan_of(sample.position)); + m_max = glm::max(m_max, plan_of(sample.position)); + } + m_min -= glm::dvec2(Margin + cell); + m_max += glm::dvec2(Margin + cell); + m_columns = static_cast((m_max.x - m_min.x) / cell) + 1; + m_rows = static_cast((m_max.y - m_min.y) / cell) + 1; + m_cells.resize(static_cast(m_columns) * m_rows); + for (std::size_t i = 0; i < Samples.size(); ++i) + m_cells[index(cell_of(plan_of(Samples[i].position)))].push_back(i); + } + + static glm::dvec2 normal(editor_track::route_sample const &Sample) + { + return {-Sample.direction.y, Sample.direction.x}; + } + + // whether the sphere may reach the gauge + bool near(glm::dvec3 const &Center, double const Radius) const + { + auto const center{plan_of(Center)}; + if (center.x + Radius < m_min.x || center.x - Radius > m_max.x || center.y + Radius < m_min.y || center.y - Radius > m_max.y) + return false; + auto const reach{(Radius + m_margin) * (Radius + m_margin)}; + return std::any_of(m_samples.begin(), m_samples.end(), [&](editor_track::route_sample const &Sample) { return glm::length2(plan_of(Sample.position) - center) <= reach; }); + } + + std::optional locate(glm::dvec3 const &Point) const + { + auto const point{plan_of(Point)}; + auto const cell{cell_of(point)}; + std::size_t nearest{m_samples.size()}; + double best{m_margin * m_margin}; + for (int row = std::max(0, cell.y - 1); row <= std::min(m_rows - 1, cell.y + 1); ++row) + for (int column = std::max(0, cell.x - 1); column <= std::min(m_columns - 1, cell.x + 1); ++column) + for (auto const i : m_cells[index({column, row})]) + { + auto const distance{glm::length2(plan_of(m_samples[i].position) - point)}; + if (distance < best) + { + best = distance; + nearest = i; + } + } + if (nearest == m_samples.size()) + return std::nullopt; + auto const &sample{m_samples[nearest]}; + auto const offset{point - plan_of(sample.position)}; + auto const along{glm::dot(offset, sample.direction)}; + if ((nearest == 0 && along < -0.6) || (nearest + 1 == m_samples.size() && along > 0.6)) + return std::nullopt; + return local{nearest, glm::dot(offset, normal(sample)), Point.y - (sample.position.y + sample.grade * along)}; + } + +private: + static constexpr double cell{8.0}; + + glm::ivec2 cell_of(glm::dvec2 const &Point) const + { + return {std::clamp(static_cast((Point.x - m_min.x) / cell), 0, m_columns - 1), std::clamp(static_cast((Point.y - m_min.y) / cell), 0, m_rows - 1)}; + } + std::size_t index(glm::ivec2 const &Cell) const + { + return static_cast(Cell.y) * m_columns + Cell.x; + } + + std::vector const &m_samples; + double m_margin; + glm::dvec2 m_min, m_max; + int m_columns{1}, m_rows{1}; + std::vector> m_cells; +}; + +gauge::section section_of(editor_track::route_sample const &Sample) +{ + gauge::section section; + if (std::abs(Sample.curvature) > 1e-5) + { + section.radius = 1.0 / std::abs(Sample.curvature); + section.inner = Sample.curvature > 0.0 ? 1 : -1; + } + section.cant = Sample.cant; + return section; +} + +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); + } +} + +} // namespace + +void editor_mode::gauge_load() +{ + if (m_gauge.profiles.empty()) + m_gauge.profiles = gauge::load_profiles(gauge_file); + m_gauge.profile = std::clamp(m_gauge.profile, 0, static_cast(m_gauge.profiles.size()) - 1); +} + +void editor_mode::update_gauge() +{ + auto *track{m_gauge.enabled && ui()->mode() == nodebank_panel::TRACK ? selected_track() : nullptr}; + if (track == nullptr) + { + if (false == EditorOverlay.batches.empty()) + EditorOverlay.clear(); + m_gauge.track = nullptr; + return; + } + // the geometry is rescanned once the edit ends + if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging) + { + m_gauge.pending = true; + return; + } + if (track == m_gauge.track && m_history.size() == m_gauge.history && false == m_gauge.pending) + return; + m_gauge.track = track; + m_gauge.history = m_history.size(); + m_gauge.pending = false; + scan_gauge(*track); +} + +void editor_mode::scan_gauge(TTrack &Track) +{ + gauge_load(); + EditorOverlay.clear(); + m_gauge.intrusions = 0; + m_gauge.models = 0; + auto route{editor_track::run_route(Track, m_gauge.reach)}; + auto const samples{editor_track::sample_route(route, 1.0)}; + auto const &profile{m_gauge.profiles[m_gauge.profile]}; + if (samples.size() < 2 || profile.outline.size() < 2) + return; + + double widest{0.0}, top{0.0}; + for (auto const &point : profile.outline) + { + widest = std::max(widest, point.half_width); + top = std::max(top, point.height); + } + corridor const space{samples, widest + gauge::curve_widening(150.0) + gauge::cant_widening(0.2, top) + 0.5}; + auto const origin{samples.front().position}; + EditorOverlay.origin = origin; + auto const relative{[&](glm::dvec3 const &Point) { return glm::vec3(Point - origin); }}; + auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { + auto const normal{corridor::normal(Sample)}; + return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); + }}; + + // outline across the track, from the bottom right up and down to the bottom left + auto const ring{[&](editor_track::route_sample const &Sample) { + auto const section{section_of(Sample)}; + std::vector points; + for (int side : {-1, 1}) + for (std::size_t j = 0; j < profile.outline.size(); ++j) + { + auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; + auto width{point.half_width + gauge::curve_widening(section.radius)}; + if (side == section.inner) + width += gauge::cant_widening(section.cant, point.height); + points.push_back(relative(lift(Sample, side * width, point.height))); + } + return points; + }}; + + gfx::editor_overlay::batch surface{{0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES}; + gfx::editor_overlay::batch lines{{0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES}; + gfx::editor_overlay::batch intruding{{1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true}; + std::vector previous; + double lastring{-1e9}; + for (std::size_t i = 0; i < samples.size(); i += 2) + { + auto const current{ring(samples[i])}; + if (false == previous.empty()) + for (std::size_t j = 1; j < current.size(); ++j) + { + surface.points.insert(surface.points.end(), {previous[j - 1], previous[j], current[j], previous[j - 1], current[j], current[j - 1]}); + lines.points.insert(lines.points.end(), {previous[j], current[j]}); + } + if (samples[i].chainage - lastring >= 10.0 || i + 2 >= samples.size()) + { + lastring = samples[i].chainage; + for (std::size_t j = 1; j < current.size(); ++j) + lines.points.insert(lines.points.end(), {current[j - 1], current[j]}); + } + previous = current; + } + + // triangle enters the gauge when any point of a grid spread over it does + auto const inside{[&](glm::dvec3 const &Point) { + auto const local{space.locate(Point)}; + return local && gauge::intrusion(profile, section_of(samples[local->sample]), local->lateral, local->height) > 0.0; + }}; + for (auto *instance : simulation::Instances.sequence()) + { + if (intruding.points.size() >= 3 * gauge_triangle_limit) + break; + if (instance == nullptr || false == instance->visible() || instance->Model() == nullptr || instance->Model()->GetSMRoot() == nullptr) + continue; + if (false == space.near(instance->location(), instance->radius())) + continue; + auto const before{intruding.points.size()}; + visit_triangles(instance->Model()->GetSMRoot(), placement(*instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { + 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 a{static_cast(u) / steps}; + auto const b{static_cast(v) / steps}; + if (inside(A + (B - A) * a + (C - A) * b)) + { + intruding.points.insert(intruding.points.end(), {relative(A), relative(B), relative(C)}); + return; + } + } + }); + if (intruding.points.size() > before) + ++m_gauge.models; + } + m_gauge.intrusions = intruding.points.size() / 3; + EditorOverlay.batches = {std::move(surface), std::move(lines), std::move(intruding)}; +} + +void editor_mode::render_gauge_ui() +{ + if (false == ImGui::CollapsingHeader("Structure gauge")) + return; + if (ImGui::Checkbox("Show in the 3D view", &m_gauge.enabled)) + m_gauge.pending = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Translucent tunnel of the gauge along the line of the selected path,\nwith the triangles of the models which enter it in red. Widened in curves and on the inner side of a cant."); + gauge_load(); + auto const previous{m_gauge.profile}; + for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) + { + if (i > 0) + ImGui::SameLine(); + ImGui::RadioButton(m_gauge.profiles[i].name.c_str(), &m_gauge.profile, i); + } + bool changed{previous != m_gauge.profile}; + float reach{static_cast(m_gauge.reach)}; + if (ImGui::SliderFloat("Reach each way (m)", &reach, 50.0f, 2000.0f, "%.0f")) + m_gauge.reach = reach; + changed |= ImGui::IsItemDeactivatedAfterEdit(); + if (m_gauge.enabled) + { + if (m_gauge.models > 0) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%zu models enter the gauge (%zu triangles)", m_gauge.models, m_gauge.intrusions); + else + ImGui::TextDisabled("The gauge is clear"); + } + + if (ImGui::TreeNode("Outline (right half)")) + { + ImGui::TextDisabled("Approximate starting points, check against PN-69/K-02057.\nMetres from the track axis and above the rail top, bottom up."); + auto &profile{m_gauge.profiles[m_gauge.profile]}; + 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(profile.outline.size()); ++i) + { + ImGui::PushID(i); + auto &point{profile.outline[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{profile.outline[std::min(i + 1, profile.outline.size() - 1)]}; + profile.outline.insert(profile.outline.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 (profile.outline.size() > 2 && ImGui::SmallButton("x")) + remove = i; + ImGui::NextColumn(); + ImGui::PopID(); + if (inserted) + break; + } + ImGui::Columns(1); + if (remove >= 0) + { + profile.outline.erase(profile.outline.begin() + remove); + edited = true; + } + if (ImGui::SmallButton("Restore the starting outlines")) + { + m_gauge.profiles = gauge::default_profiles(); + edited = true; + } + if (edited) + { + gauge::save_profiles(gauge_file, m_gauge.profiles); + changed = true; + } + ImGui::TreePop(); + } + if (changed) + m_gauge.pending = true; +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4e3b9a94..2fb4ace3 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -482,6 +482,7 @@ void editor_mode::render_track_window() m_infra.scope = 1; infra_recognize(); } + render_gauge_ui(); ImGui::End(); } diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp new file mode 100644 index 00000000..937feb5b --- /dev/null +++ b/editor/editorGauge.cpp @@ -0,0 +1,110 @@ +/* +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 + +namespace gauge +{ + +std::vector default_profiles() +{ + std::vector const lower{{1.645, 0.05}, {1.645, 0.30}, {1.720, 0.30}, {1.720, 1.10}, {2.200, 1.10}, {2.200, 3.85}, {1.900, 4.25}}; + profile plain{"A (bez sieci)", lower}; + plain.outline.insert(plain.outline.end(), {{1.400, 4.85}, {0.0, 4.85}}); + profile electrified{"C (sieć trakcyjna)", lower}; + electrified.outline.insert(electrified.outline.end(), {{1.700, 4.85}, {1.400, 5.60}, {1.000, 6.00}, {0.0, 6.00}}); + return {plain, electrified}; +} + +// one outline per line: the name in quotes, 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; + if (false == static_cast(stream >> std::quoted(profile.name))) + continue; + 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); + for (auto const &point : profile.outline) + output << ' ' << point.half_width << ' ' << point.height; + output << '\n'; + } +} + +double curve_widening(double const Radius) +{ + // smallest radius of the class and its widening in mm; the radius between the classes takes the larger one + static constexpr std::pair table[]{{3500, 10}, {2500, 15}, {1800, 20}, {1500, 25}, {1200, 30}, {1000, 35}, + {900, 40}, {800, 45}, {700, 50}, {600, 60}, {500, 75}, {450, 80}, + {400, 90}, {350, 105}, {300, 120}, {280, 130}, {260, 140}, {250, 145}, + {240, 150}, {220, 165}, {200, 180}, {190, 190}, {180, 200}}; + if (Radius <= 0.0 || Radius >= 4000.0) + return 0.0; + for (auto const &[radius, widening] : table) + if (Radius >= radius) + return widening * 0.001; + return 36.0 / Radius; +} + +double cant_widening(double const Cant, double const Height) +{ + return Height * Cant / 1.5; +} + +double half_width(profile const &Profile, section const &Section, double const Height, int const Side) +{ + double width{-1.0}; + for (std::size_t i = 1; i < Profile.outline.size(); ++i) + { + auto const &a{Profile.outline[i - 1]}; + auto const &b{Profile.outline[i]}; + if (Height < std::min(a.height, b.height) || Height > std::max(a.height, b.height)) + continue; + auto const span{b.height - a.height}; + width = std::max(width, std::abs(span) < 1e-9 ? std::max(a.half_width, b.half_width) : a.half_width + (b.half_width - a.half_width) * (Height - a.height) / span); + } + if (width < 0.0) + return width; + width += curve_widening(Section.radius); + if (Section.inner != 0 && Side == Section.inner) + width += cant_widening(Section.cant, Height); + return width; +} + +double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) +{ + auto const width{half_width(Profile, Section, Height, Lateral >= 0.0 ? 1 : -1)}; + if (width < 0.0) + return 0.0; + return width - std::abs(Lateral); +} + +} // namespace gauge diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp new file mode 100644 index 00000000..e4d235bc --- /dev/null +++ b/editor/editorGauge.hpp @@ -0,0 +1,55 @@ +/* +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 + +// structure gauge (skrajnia budowli): the space along the track which has to stay clear of the objects +namespace gauge +{ + +// right half of the outline on a straight track, in metres: half-width from the axis and height above the rail top, +// listed from the bottom up; the left half is the mirror +struct profile +{ + std::string name; + struct point + { + double half_width{0.0}; + double height{0.0}; + }; + std::vector outline; +}; + +// starting points of the outlines, approximations to verify against PN-69/K-02057 +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); + +// widening of both sides in a curve, from the radius (table 1 of the D1 annex 11), metres +double curve_widening(double Radius); +// widening of the inner side of a curve from the tilt of the vehicles on the cant, at the height above the rail top, metres +double cant_widening(double Cant, double Height); + +// place of the gauge across the track +struct section +{ + double radius{0.0}; // 0: straight + int inner{0}; // side of the curve centre, +1 left or -1 right, 0 on a straight + double cant{0.0}; // metres +}; +// half-width of the gauge at the height on the side (+1 left, -1 right), negative above or below the outline +double half_width(profile const &Profile, section const &Section, double Height, int Side); +// depth of the point inside the gauge, metres, not positive outside of it. Lateral positive to the left +double intrusion(profile const &Profile, section const &Section, double Lateral, double Height); + +} // namespace gauge diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index e328cbb3..d43d3043 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1406,7 +1406,8 @@ std::vector editor_track::sample_route(route &Route, for (std::size_t i = 0; i < Route.spans.size(); ++i) { auto &span{Route.spans[i]}; - bezier const curve{span.track->m_paths[span.path]}; + 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))}; @@ -1425,6 +1426,7 @@ std::vector editor_track::sample_route(route &Route, 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; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 742e393d..d45ecf54 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -175,6 +175,7 @@ class editor_track 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); diff --git a/rendering/editoroverlay.h b/rendering/editoroverlay.h new file mode 100644 index 00000000..6a533165 --- /dev/null +++ b/rendering/editoroverlay.h @@ -0,0 +1,42 @@ +/* +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 + bool offset{false}; // pulled towards the camera, to show over the coplanar surfaces + 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 682ca59e..4c769e8f 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(); @@ -4489,6 +4491,61 @@ 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; + if (batches[i].offset) + { + ::glEnable(GL_POLYGON_OFFSET_FILL); + ::glPolygonOffset(-2.f, -2.f); + } + model_ubs.param[0] = batches[i].color; + draw(m_editor_overlay_geometry[i]); + ::glDisable(GL_POLYGON_OFFSET_FILL); + ++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 9d9bba8c..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); @@ -449,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; From cf8062ea966f0fd84f8053f917a248a2a487315f Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:40:25 +0200 Subject: [PATCH 21/34] feat: GPL-1/GPL-2 structure gauges, widening run-in ahead of the curves --- application/editormodegauge.cpp | 46 +++++----- editor/editorGauge.cpp | 147 ++++++++++++++++++++++++++------ editor/editorGauge.hpp | 28 +++--- 3 files changed, 161 insertions(+), 60 deletions(-) diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 2fbd9192..731cf6cc 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -30,7 +30,7 @@ using geometry::plan_of; namespace { -char const *const gauge_file{"editor_gauges.txt"}; +char const *const gauge_file{"editor_structure_gauges.txt"}; std::size_t const gauge_triangle_limit{200000}; // samples of the route, found by the position in the plan @@ -122,16 +122,16 @@ private: std::vector> m_cells; }; -gauge::section section_of(editor_track::route_sample const &Sample) +std::vector sections_of(std::vector const &Samples) { - gauge::section section; - if (std::abs(Sample.curvature) > 1e-5) + std::vector chainage, curvature, cant; + for (auto const &sample : Samples) { - section.radius = 1.0 / std::abs(Sample.curvature); - section.inner = Sample.curvature > 0.0 ? 1 : -1; + chainage.push_back(sample.chainage); + curvature.push_back(sample.curvature); + cant.push_back(sample.cant); } - section.cant = Sample.cant; - return section; + return gauge::sections(chainage, curvature, cant); } glm::dmat4 placement(TAnimModel const &Instance) @@ -217,13 +217,12 @@ void editor_mode::scan_gauge(TTrack &Track) if (samples.size() < 2 || profile.outline.size() < 2) return; - double widest{0.0}, top{0.0}; + auto const sections{sections_of(samples)}; + double widest{0.0}; for (auto const &point : profile.outline) - { widest = std::max(widest, point.half_width); - top = std::max(top, point.height); - } - corridor const space{samples, widest + gauge::curve_widening(150.0) + gauge::cant_widening(0.2, top) + 0.5}; + // room for the widening in the sharpest curves on the largest cant + corridor const space{samples, widest + 1.2}; auto const origin{samples.front().position}; EditorOverlay.origin = origin; auto const relative{[&](glm::dvec3 const &Point) { return glm::vec3(Point - origin); }}; @@ -233,17 +232,14 @@ void editor_mode::scan_gauge(TTrack &Track) }}; // outline across the track, from the bottom right up and down to the bottom left - auto const ring{[&](editor_track::route_sample const &Sample) { - auto const section{section_of(Sample)}; + auto const ring{[&](std::size_t const Index) { std::vector points; for (int side : {-1, 1}) for (std::size_t j = 0; j < profile.outline.size(); ++j) { auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; - auto width{point.half_width + gauge::curve_widening(section.radius)}; - if (side == section.inner) - width += gauge::cant_widening(section.cant, point.height); - points.push_back(relative(lift(Sample, side * width, point.height))); + auto const width{point.half_width + gauge::widening(sections[Index], point.height, side)}; + points.push_back(relative(lift(samples[Index], side * width, point.height))); } return points; }}; @@ -255,7 +251,7 @@ void editor_mode::scan_gauge(TTrack &Track) double lastring{-1e9}; for (std::size_t i = 0; i < samples.size(); i += 2) { - auto const current{ring(samples[i])}; + auto const current{ring(i)}; if (false == previous.empty()) for (std::size_t j = 1; j < current.size(); ++j) { @@ -274,7 +270,7 @@ void editor_mode::scan_gauge(TTrack &Track) // triangle enters the gauge when any point of a grid spread over it does auto const inside{[&](glm::dvec3 const &Point) { auto const local{space.locate(Point)}; - return local && gauge::intrusion(profile, section_of(samples[local->sample]), local->lateral, local->height) > 0.0; + return local && gauge::intrusion(profile, sections[local->sample], local->lateral, local->height) > 0.0; }}; for (auto *instance : simulation::Instances.sequence()) { @@ -314,13 +310,13 @@ void editor_mode::render_gauge_ui() if (ImGui::Checkbox("Show in the 3D view", &m_gauge.enabled)) m_gauge.pending = true; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Translucent tunnel of the gauge along the line of the selected path,\nwith the triangles of the models which enter it in red. Widened in curves and on the inner side of a cant."); + ImGui::SetTooltip("Translucent tunnel of the gauge along the line of the selected path,\nwith the triangles of the models which enter it in red.\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(); auto const previous{m_gauge.profile}; for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) { - if (i > 0) - ImGui::SameLine(); + if (i % 2 == 1) + ImGui::SameLine(160.0f); ImGui::RadioButton(m_gauge.profiles[i].name.c_str(), &m_gauge.profile, i); } bool changed{previous != m_gauge.profile}; @@ -338,7 +334,7 @@ void editor_mode::render_gauge_ui() if (ImGui::TreeNode("Outline (right half)")) { - ImGui::TextDisabled("Approximate starting points, check against PN-69/K-02057.\nMetres from the track axis and above the rail top, bottom up."); + ImGui::TextDisabled("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); auto &profile{m_gauge.profiles[m_gauge.profile]}; bool edited{false}; bool inserted{false}; diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp index 937feb5b..59acae31 100644 --- a/editor/editorGauge.cpp +++ b/editor/editorGauge.cpp @@ -16,14 +16,94 @@ http://mozilla.org/MPL/2.0/. 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; +}; + +} // namespace + std::vector default_profiles() { - std::vector const lower{{1.645, 0.05}, {1.645, 0.30}, {1.720, 0.30}, {1.720, 1.10}, {2.200, 1.10}, {2.200, 3.85}, {1.900, 4.25}}; - profile plain{"A (bez sieci)", lower}; - plain.outline.insert(plain.outline.end(), {{1.400, 4.85}, {0.0, 4.85}}); - profile electrified{"C (sieć trakcyjna)", lower}; - electrified.outline.insert(electrified.outline.end(), {{1.700, 4.85}, {1.400, 5.60}, {1.000, 6.00}, {0.0, 6.00}}); - return {plain, electrified}; + // limit installation gauge up to 1170 mm, then the outline of the unified gauge + std::vector const lower{{1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}, {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, lower}; + for (auto const &part : Parts) + result.outline.insert(result.outline.end(), part.begin(), part.end()); + return result; + }}; + 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})}; } // one outline per line: the name in quotes, then pairs of half-width and height @@ -59,24 +139,46 @@ void save_profiles(std::string const &File, std::vector const &Profiles } } -double curve_widening(double const Radius) +std::vector
sections(std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant) { - // smallest radius of the class and its widening in mm; the radius between the classes takes the larger one - static constexpr std::pair table[]{{3500, 10}, {2500, 15}, {1800, 20}, {1500, 25}, {1200, 30}, {1000, 35}, - {900, 40}, {800, 45}, {700, 50}, {600, 60}, {500, 75}, {450, 80}, - {400, 90}, {350, 105}, {300, 120}, {280, 130}, {260, 140}, {250, 145}, - {240, 150}, {220, 165}, {200, 180}, {190, 190}, {180, 200}}; - if (Radius <= 0.0 || Radius >= 4000.0) - return 0.0; - for (auto const &[radius, widening] : table) - if (Radius >= radius) - return widening * 0.001; - return 36.0 / Radius; + auto const count{Chainage.size()}; + std::vector
result(count); + if (count == 0) + 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{upper_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]); + } + } + return result; } -double cant_widening(double const Cant, double const Height) +double widening(section const &Section, double const Height, int const Side) { - return Height * Cant / 1.5; + auto const i{Side > 0 ? 1 : 0}; + return (Height < lower_part ? Section.lower[i] : Section.upper[i]) + Section.cant[i] * Height / 1.5; } double half_width(profile const &Profile, section const &Section, double const Height, int const Side) @@ -93,10 +195,7 @@ double half_width(profile const &Profile, section const &Section, double const H } if (width < 0.0) return width; - width += curve_widening(Section.radius); - if (Section.inner != 0 && Side == Section.inner) - width += cant_widening(Section.cant, Height); - return width; + return width + widening(Section, Height, Side); } double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp index e4d235bc..7ef65bbf 100644 --- a/editor/editorGauge.hpp +++ b/editor/editorGauge.hpp @@ -9,10 +9,12 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include #include #include -// structure gauge (skrajnia budowli): the space along the track which has to stay clear of the objects +// 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 { @@ -29,25 +31,29 @@ struct profile std::vector outline; }; -// starting points of the outlines, approximations to verify against PN-69/K-02057 +// GPL-1 and GPL-2 over the limit installation gauge below 1170 mm, with and without the pantograph gauge 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); -// widening of both sides in a curve, from the radius (table 1 of the D1 annex 11), metres -double curve_widening(double Radius); -// widening of the inner side of a curve from the tilt of the vehicles on the cant, at the height above the rail top, metres -double cant_widening(double Cant, double Height); +// below this height the outline is the limit installation gauge, which leaves the room for the platforms +double constexpr lower_part{1.17}; -// place of the gauge across the track +// 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 radius{0.0}; // 0: straight - int inner{0}; // side of the curve centre, +1 left or -1 right, 0 on a straight - double cant{0.0}; // metres + 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 }; -// half-width of the gauge at the height on the side (+1 left, -1 right), negative above or below the outline +// 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 +std::vector
sections(std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); + +// widening of the side (+1 left, -1 right) at the height +double widening(section const &Section, double Height, int Side); +// half-width of the gauge at the height on the side, negative above or below the outline double half_width(profile const &Profile, section const &Section, double Height, int Side); // depth of the point inside the gauge, metres, not positive outside of it. Lateral positive to the left double intrusion(profile const &Profile, section const &Section, double Lateral, double Height); From f2e727ce9a2357993fa66eb4b8f0bbd0fbee1fe5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:44:04 +0200 Subject: [PATCH 22/34] feat: structure gauge violations on the whole map, camera goes to them --- application/editormode.h | 32 +- application/editormodegauge.cpp | 606 +++++++++++++++++++++----------- 2 files changed, 431 insertions(+), 207 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 7bdc8c2c..f6dbe166 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -39,6 +39,13 @@ 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 + }; // constructors editor_mode(); ~editor_mode() override; @@ -316,13 +323,34 @@ class editor_mode : public application_mode, private editor_track::observer TTrack const *track{nullptr}; // scanned std::size_t history{0}; bool pending{false}; // rescan once the edit ends - std::size_t models{0}; - std::size_t intrusions{0}; // triangles + struct + { + std::vector surface; // triangles of the tunnel + std::vector edges; // lines + std::vector intruding; // triangles of the models + std::vector hits; + } line; // along the selected path + struct + { + bool scanned{false}; + std::size_t history{0}; + std::string profile; + std::vector intruding; + std::vector hits; + } map; // whole scenery + int current{-1}; // hit the camera went to + bool published{false}; // overlay up to date } m_gauge; void gauge_load(); + std::vector const &gauge_hits() const; void update_gauge(); void scan_gauge(TTrack &Track); + void scan_gauge_map(); + void gauge_publish(); + void gauge_focus(int Index); void render_gauge_ui(); + void render_gauge_hits(); + bool render_gauge_outline(); struct route_design { TTrack *from{nullptr}; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 731cf6cc..53f2e889 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -17,13 +17,17 @@ http://mozilla.org/MPL/2.0/. #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 using geometry::plan_of; @@ -32,33 +36,76 @@ namespace char const *const gauge_file{"editor_structure_gauges.txt"}; std::size_t const gauge_triangle_limit{200000}; +// 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}; -// samples of the route, found by the position in the plan +// sampled line of the tracks with the widening of the gauge along it +struct gauge_line +{ + std::vector samples; + std::vector sections; +}; + +gauge_line make_line(editor_track::route &Route) +{ + gauge_line line; + line.samples = editor_track::sample_route(Route, 1.0); + std::vector chainage, curvature, cant; + for (auto const &sample : line.samples) + { + chainage.push_back(sample.chainage); + curvature.push_back(sample.curvature); + cant.push_back(sample.cant); + } + line.sections = gauge::sections(chainage, curvature, cant); + return line; +} + +// lines through all paths of the scenery, each path in one of them at least +std::vector scenery_lines() +{ + std::vector lines; + std::unordered_set visited; + auto const seen{[&](editor_track::route_span const &Span) { return visited.count(Span.track) > 0; }}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || visited.count(track) > 0 || false == editor_track::is_supported(*track)) + 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)); + } + // diverging paths of the switches + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && track->eType == tt_Switch && editor_track::is_supported(*track)) + { + editor_track::route route; + route.spans.push_back({track, 1, true}); + lines.push_back(make_line(route)); + } + return lines; +} + +// samples of the lines, found by the position in the plan class corridor { public: - struct local + corridor(std::vector const &Lines, double const Margin) : m_lines{Lines}, m_margin{Margin} { - std::size_t sample; - double lateral; // positive to the left - double height; // above the rail top - }; - - corridor(std::vector const &Samples, double const Margin) : m_samples{Samples}, m_margin{Margin} - { - m_min = m_max = plan_of(Samples.front().position); - for (auto const &sample : Samples) - { - m_min = glm::min(m_min, plan_of(sample.position)); - m_max = glm::max(m_max, plan_of(sample.position)); - } - m_min -= glm::dvec2(Margin + cell); - m_max += glm::dvec2(Margin + cell); - m_columns = static_cast((m_max.x - m_min.x) / cell) + 1; - m_rows = static_cast((m_max.y - m_min.y) / cell) + 1; - m_cells.resize(static_cast(m_columns) * m_rows); - for (std::size_t i = 0; i < Samples.size(); ++i) - m_cells[index(cell_of(plan_of(Samples[i].position)))].push_back(i); + for (std::uint32_t line = 0; line < Lines.size(); ++line) + for (std::uint32_t i = 0; i < Lines[line].samples.size(); ++i) + { + 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) @@ -70,70 +117,90 @@ public: bool near(glm::dvec3 const &Center, double const Radius) const { auto const center{plan_of(Center)}; - if (center.x + Radius < m_min.x || center.x - Radius > m_max.x || center.y + Radius < m_min.y || center.y - Radius > m_max.y) - return false; - auto const reach{(Radius + m_margin) * (Radius + m_margin)}; - return std::any_of(m_samples.begin(), m_samples.end(), [&](editor_track::route_sample const &Sample) { return glm::length2(plan_of(Sample.position) - center) <= reach; }); + 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; } - std::optional locate(glm::dvec3 const &Point) const + // deepest intrusion of the point into the gauge of the lines nearby, not positive outside of them + double intrusion(gauge::profile const &Profile, glm::dvec3 const &Point) const { auto const point{plan_of(Point)}; - auto const cell{cell_of(point)}; - std::size_t nearest{m_samples.size()}; - double best{m_margin * m_margin}; - for (int row = std::max(0, cell.y - 1); row <= std::min(m_rows - 1, cell.y + 1); ++row) - for (int column = std::max(0, cell.x - 1); column <= std::min(m_columns - 1, cell.x + 1); ++column) - for (auto const i : m_cells[index({column, row})]) + 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(m_samples[i].position) - point)}; - if (distance < best) - { - best = distance; - nearest = i; - } + 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}; } - if (nearest == m_samples.size()) - return std::nullopt; - auto const &sample{m_samples[nearest]}; - auto const offset{point - plan_of(sample.position)}; - auto const along{glm::dot(offset, sample.direction)}; - if ((nearest == 0 && along < -0.6) || (nearest + 1 == m_samples.size() && along > 0.6)) - return std::nullopt; - return local{nearest, glm::dot(offset, normal(sample)), Point.y - (sample.position.y + sample.grade * along)}; + } + double deepest{0.0}; + 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; + auto const height{Point.y - (sample.position.y + sample.grade * along)}; + deepest = std::max(deepest, gauge::intrusion(Profile, line.sections[found.index], glm::dot(offset, normal(sample)), height)); + } + return deepest; } private: static constexpr double cell{8.0}; + static constexpr double region{64.0}; - glm::ivec2 cell_of(glm::dvec2 const &Point) const + struct entry { - return {std::clamp(static_cast((Point.x - m_min.x) / cell), 0, m_columns - 1), std::clamp(static_cast((Point.y - m_min.y) / cell), 0, m_rows - 1)}; + 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))}; } - std::size_t index(glm::ivec2 const &Cell) const + static std::uint64_t key(std::int64_t const X, std::int64_t const Y) { - return static_cast(Cell.y) * m_columns + Cell.x; + 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_samples; + std::vector const &m_lines; double m_margin; - glm::dvec2 m_min, m_max; - int m_columns{1}, m_rows{1}; - std::vector> m_cells; + std::unordered_map> m_cells; + std::unordered_set m_regions; }; -std::vector sections_of(std::vector const &Samples) -{ - std::vector chainage, curvature, cant; - for (auto const &sample : Samples) - { - chainage.push_back(sample.chainage); - curvature.push_back(sample.curvature); - cant.push_back(sample.cant); - } - return gauge::sections(chainage, curvature, cant); -} - glm::dmat4 placement(TAnimModel const &Instance) { auto const angles{Instance.Angles()}; @@ -172,6 +239,53 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } } +// triangles of the models entering the gauge, and the deepest place of each model, the deepest models first +void scan_models(corridor const &Space, gauge::profile const &Profile, std::vector &Triangles, std::vector &Hits) +{ + for (auto *instance : simulation::Instances.sequence()) + { + if (Triangles.size() >= 3 * gauge_triangle_limit) + break; + if (instance == nullptr || false == instance->visible() || instance->Model() == nullptr || instance->Model()->GetSMRoot() == nullptr) + continue; + if (false == Space.near(instance->location(), instance->radius())) + continue; + editor_mode::gauge_hit hit{instance->name(), instance->location(), 0.0}; + // 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 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 depth{Space.intrusion(Profile, point)}; + if (depth <= 0.0) + continue; + Triangles.insert(Triangles.end(), {A, B, C}); + if (depth > hit.depth) + { + hit.depth = depth; + hit.point = point; + } + return; + } + }); + if (hit.depth > 0.0) + Hits.push_back(std::move(hit)); + } + std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); +} + +double widest(gauge::profile const &Profile) +{ + double result{0.0}; + for (auto const &point : Profile.outline) + result = std::max(result, point.half_width); + // with the room for the widening in the sharpest curves on the largest cant + return result + 1.2; +} + } // namespace void editor_mode::gauge_load() @@ -181,133 +295,172 @@ void editor_mode::gauge_load() m_gauge.profile = std::clamp(m_gauge.profile, 0, static_cast(m_gauge.profiles.size()) - 1); } +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() { - auto *track{m_gauge.enabled && ui()->mode() == nodebank_panel::TRACK ? selected_track() : nullptr}; - if (track == nullptr) + if (ui()->mode() != nodebank_panel::TRACK) { if (false == EditorOverlay.batches.empty()) EditorOverlay.clear(); - m_gauge.track = nullptr; + 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; - return; + else if (track == nullptr) + { + if (m_gauge.track != nullptr) + { + m_gauge.line = {}; + m_gauge.track = nullptr; + m_gauge.published = false; + } } - if (track == m_gauge.track && m_history.size() == m_gauge.history && false == m_gauge.pending) - return; - m_gauge.track = track; - m_gauge.history = m_history.size(); - m_gauge.pending = false; - scan_gauge(*track); + 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(); - EditorOverlay.clear(); - m_gauge.intrusions = 0; - m_gauge.models = 0; - auto route{editor_track::run_route(Track, m_gauge.reach)}; - auto const samples{editor_track::sample_route(route, 1.0)}; + m_gauge.line = {}; auto const &profile{m_gauge.profiles[m_gauge.profile]}; - if (samples.size() < 2 || profile.outline.size() < 2) + auto route{editor_track::run_route(Track, m_gauge.reach)}; + std::vector const lines{make_line(route)}; + auto const &line{lines.front()}; + if (line.samples.size() < 2 || profile.outline.size() < 2) return; + corridor const space{lines, widest(profile)}; - auto const sections{sections_of(samples)}; - double widest{0.0}; - for (auto const &point : profile.outline) - widest = std::max(widest, point.half_width); - // room for the widening in the sharpest curves on the largest cant - corridor const space{samples, widest + 1.2}; - auto const origin{samples.front().position}; - EditorOverlay.origin = origin; - auto const relative{[&](glm::dvec3 const &Point) { return glm::vec3(Point - origin); }}; auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { auto const normal{corridor::normal(Sample)}; return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); }}; - // outline across the track, from the bottom right up and down to the bottom left auto const ring{[&](std::size_t const Index) { - std::vector points; + std::vector points; for (int side : {-1, 1}) for (std::size_t j = 0; j < profile.outline.size(); ++j) { auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; - auto const width{point.half_width + gauge::widening(sections[Index], point.height, side)}; - points.push_back(relative(lift(samples[Index], side * width, point.height))); + auto const width{point.half_width + gauge::widening(line.sections[Index], point.height, side)}; + points.push_back(lift(line.samples[Index], side * width, point.height)); } return points; }}; - - gfx::editor_overlay::batch surface{{0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES}; - gfx::editor_overlay::batch lines{{0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES}; - gfx::editor_overlay::batch intruding{{1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true}; - std::vector previous; + auto &surface{m_gauge.line.surface}; + auto &edges{m_gauge.line.edges}; + std::vector previous; double lastring{-1e9}; - for (std::size_t i = 0; i < samples.size(); i += 2) + 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.points.insert(surface.points.end(), {previous[j - 1], previous[j], current[j], previous[j - 1], current[j], current[j - 1]}); - lines.points.insert(lines.points.end(), {previous[j], current[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 (samples[i].chainage - lastring >= 10.0 || i + 2 >= samples.size()) + if (line.samples[i].chainage - lastring >= 10.0 || i + 2 >= line.samples.size()) { - lastring = samples[i].chainage; + lastring = line.samples[i].chainage; for (std::size_t j = 1; j < current.size(); ++j) - lines.points.insert(lines.points.end(), {current[j - 1], current[j]}); + edges.insert(edges.end(), {current[j - 1], current[j]}); } previous = current; } + scan_models(space, profile, m_gauge.line.intruding, m_gauge.line.hits); + if (false == m_gauge.map.scanned) + m_gauge.current = -1; +} - // triangle enters the gauge when any point of a grid spread over it does - auto const inside{[&](glm::dvec3 const &Point) { - auto const local{space.locate(Point)}; - return local && gauge::intrusion(profile, sections[local->sample], local->lateral, local->height) > 0.0; +void editor_mode::scan_gauge_map() +{ + gauge_load(); + auto const started{std::chrono::steady_clock::now()}; + m_gauge.map = {}; + auto const &profile{m_gauge.profiles[m_gauge.profile]}; + auto const lines{scenery_lines()}; + corridor const space{lines, widest(profile)}; + scan_models(space, profile, m_gauge.map.intruding, m_gauge.map.hits); + m_gauge.map.scanned = true; + m_gauge.map.history = m_history.size(); + m_gauge.map.profile = profile.name; + m_gauge.current = -1; + m_gauge.published = false; + WriteLog("Structure gauge (" + profile.name + "): " + std::to_string(m_gauge.map.hits.size()) + " models enter it along " + std::to_string(lines.size()) + " lines, scanned in " + + std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()) + " ms"); + if (false == m_gauge.map.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, bool const Offset, std::vector const &Points) { + gfx::editor_overlay::batch result{Color, Type, Offset}; + result.points.reserve(Points.size()); + for (auto const &point : Points) + result.points.emplace_back(point - EditorOverlay.origin); + return result; }}; - for (auto *instance : simulation::Instances.sequence()) + std::vector marker; + auto const &hits{gauge_hits()}; + if (m_gauge.current >= 0 && m_gauge.current < static_cast(hits.size())) { - if (intruding.points.size() >= 3 * gauge_triangle_limit) - break; - if (instance == nullptr || false == instance->visible() || instance->Model() == nullptr || instance->Model()->GetSMRoot() == nullptr) - continue; - if (false == space.near(instance->location(), instance->radius())) - continue; - auto const before{intruding.points.size()}; - visit_triangles(instance->Model()->GetSMRoot(), placement(*instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { - 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 a{static_cast(u) / steps}; - auto const b{static_cast(v) / steps}; - if (inside(A + (B - A) * a + (C - A) * b)) - { - intruding.points.insert(intruding.points.end(), {relative(A), relative(B), relative(C)}); - return; - } - } - }); - if (intruding.points.size() > before) - ++m_gauge.models; + 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)}; } - m_gauge.intrusions = intruding.points.size() / 3; - EditorOverlay.batches = {std::move(surface), std::move(lines), std::move(intruding)}; + EditorOverlay.batches = {batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, false, m_gauge.line.surface), + batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, false, m_gauge.line.edges), + batch({1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true, m_gauge.line.intruding), + batch({1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true, m_gauge.map.intruding), + batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, false, marker)}; +} + +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; + auto const target{hits[m_gauge.current].point}; + // from the side the camera looks from, a bit above + glm::dvec3 away{Camera.Pos.x - target.x, 0.0, Camera.Pos.z - target.z}; + away = glm::length(away) > 1e-3 ? glm::normalize(away) : glm::dvec3(1.0, 0.0, 0.0); + m_focus_start_pos = Camera.Pos; + m_focus_start_angle = Camera.Angle; + m_focus_target_pos = target + away * 12.0 + glm::dvec3(0.0, 4.0, 0.0); + auto const look{glm::normalize(target - m_focus_target_pos)}; + 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::render_gauge_ui() { if (false == ImGui::CollapsingHeader("Structure gauge")) return; - if (ImGui::Checkbox("Show in the 3D view", &m_gauge.enabled)) + 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 along the line of the selected path,\nwith the triangles of the models which enter it in red.\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."); @@ -324,75 +477,118 @@ void editor_mode::render_gauge_ui() if (ImGui::SliderFloat("Reach each way (m)", &reach, 50.0f, 2000.0f, "%.0f")) m_gauge.reach = reach; changed |= ImGui::IsItemDeactivatedAfterEdit(); - if (m_gauge.enabled) - { - if (m_gauge.models > 0) - ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%zu models enter the gauge (%zu triangles)", m_gauge.models, m_gauge.intrusions); - else - ImGui::TextDisabled("The gauge is clear"); - } - if (ImGui::TreeNode("Outline (right half)")) + if (ImGui::Button("Find the violations on the whole map")) + scan_gauge_map(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Checks the models along all tracks of the scenery against the chosen gauge,\nmarks them in red and lists them, the deepest first"); + if (m_gauge.map.scanned) { - ImGui::TextDisabled("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); - auto &profile{m_gauge.profiles[m_gauge.profile]}; - 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(profile.outline.size()); ++i) + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { - ImGui::PushID(i); - auto &point{profile.outline[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{profile.outline[std::min(i + 1, profile.outline.size() - 1)]}; - profile.outline.insert(profile.outline.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 (profile.outline.size() > 2 && ImGui::SmallButton("x")) - remove = i; - ImGui::NextColumn(); - ImGui::PopID(); - if (inserted) - break; + m_gauge.map = {}; + m_gauge.current = -1; + m_gauge.published = false; } - ImGui::Columns(1); - if (remove >= 0) - { - profile.outline.erase(profile.outline.begin() + remove); - edited = true; - } - if (ImGui::SmallButton("Restore the starting outlines")) - { - m_gauge.profiles = gauge::default_profiles(); - edited = true; - } - if (edited) - { - gauge::save_profiles(gauge_file, m_gauge.profiles); - changed = true; - } - ImGui::TreePop(); + 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(); if (changed) m_gauge.pending = true; } + +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_outline() +{ + if (false == ImGui::TreeNode("Outline (right half)")) + return false; + ImGui::TextDisabled("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); + auto &profile{m_gauge.profiles[m_gauge.profile]}; + 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(profile.outline.size()); ++i) + { + ImGui::PushID(i); + auto &point{profile.outline[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{profile.outline[std::min(i + 1, profile.outline.size() - 1)]}; + profile.outline.insert(profile.outline.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 (profile.outline.size() > 2 && ImGui::SmallButton("x")) + remove = i; + ImGui::NextColumn(); + ImGui::PopID(); + if (inserted) + break; + } + ImGui::Columns(1); + if (remove >= 0) + { + profile.outline.erase(profile.outline.begin() + remove); + edited = true; + } + if (ImGui::SmallButton("Restore the starting outlines")) + { + m_gauge.profiles = gauge::default_profiles(); + edited = true; + } + if (edited) + gauge::save_profiles(gauge_file, m_gauge.profiles); + ImGui::TreePop(); + return edited; +} From 208ad3400d1b3b0a8a0548f408ddbe92b8b0f3fc Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:48:11 +0200 Subject: [PATCH 23/34] fix: structure gauge measured from the rail head, not the rail foot of the scenery points --- application/editormodegauge.cpp | 3 ++- editor/editorGauge.hpp | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 53f2e889..f01ddce6 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -51,8 +51,9 @@ gauge_line make_line(editor_track::route &Route) gauge_line line; line.samples = editor_track::sample_route(Route, 1.0); std::vector chainage, curvature, cant; - for (auto const &sample : line.samples) + for (auto &sample : line.samples) { + sample.position.y += gauge::rail_height; chainage.push_back(sample.chainage); curvature.push_back(sample.curvature); cant.push_back(sample.cant); diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp index 7ef65bbf..ee9b6be8 100644 --- a/editor/editorGauge.hpp +++ b/editor/editorGauge.hpp @@ -37,6 +37,9 @@ std::vector default_profiles(); 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}; From d57215877f61259f19ed911415ba500243829ba8 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:54:17 +0200 Subject: [PATCH 24/34] feat: structure gauge window in the menu bar, gauge tunnel around the hit, fixed reach --- application/editormode.cpp | 2 + application/editormode.h | 14 ++- application/editormodegauge.cpp | 158 ++++++++++++++++++++------------ application/editormodetrack.cpp | 1 - application/editoruilayer.cpp | 6 ++ application/editoruilayer.h | 6 ++ 6 files changed, 124 insertions(+), 63 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index f074dc05..dc90ca72 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -340,6 +340,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); // the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) { gather_ground_triangles(Min, Max, true, 50.0f, Out); @@ -1036,6 +1037,7 @@ bool editor_mode::update() render_infra_window(); } update_gauge(); + render_gauge_window(); // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) diff --git a/application/editormode.h b/application/editormode.h index f6dbe166..4bef2f71 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -45,6 +45,8 @@ class editor_mode : public application_mode, private editor_track::observer 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}; }; // constructors editor_mode(); @@ -316,10 +318,10 @@ class editor_mode : public application_mode, private editor_track::observer // structure gauge along the line of the selected path, shown in the 3d view struct gauge_state { - bool enabled{false}; + bool open{false}; // window + bool enabled{true}; // tunnel along the selected path std::vector profiles; int profile{0}; - double reach{500.0}; // m, each way from the path TTrack const *track{nullptr}; // scanned std::size_t history{0}; bool pending{false}; // rescan once the edit ends @@ -338,6 +340,11 @@ class editor_mode : public application_mode, private editor_track::observer std::vector intruding; 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 } m_gauge; @@ -348,7 +355,8 @@ class editor_mode : public application_mode, private editor_track::observer void scan_gauge_map(); void gauge_publish(); void gauge_focus(int Index); - void render_gauge_ui(); + void gauge_spot(gauge_hit const &Hit); + void render_gauge_window(); void render_gauge_hits(); bool render_gauge_outline(); struct route_design diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index f01ddce6..2a07a950 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -38,10 +38,14 @@ char const *const gauge_file{"editor_structure_gauges.txt"}; std::size_t const gauge_triangle_limit{200000}; // 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}; // sampled line of the tracks with the widening of the gauge along it struct gauge_line { + editor_track::route route; std::vector samples; std::vector sections; }; @@ -50,6 +54,7 @@ gauge_line make_line(editor_track::route &Route) { gauge_line line; line.samples = editor_track::sample_route(Route, 1.0); + line.route = Route; std::vector chainage, curvature, cant; for (auto &sample : line.samples) { @@ -128,8 +133,13 @@ public: return false; } - // deepest intrusion of the point into the gauge of the lines nearby, not positive outside of them - double intrusion(gauge::profile const &Profile, glm::dvec3 const &Point) const + 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 + }; + // deepest intrusion of the point into the gauge of the lines nearby + hit intrusion(gauge::profile const &Profile, glm::dvec3 const &Point) const { auto const point{plan_of(Point)}; auto const center{index(point, cell)}; @@ -153,7 +163,7 @@ public: *same = {candidate, distance}; } } - double deepest{0.0}; + hit deepest; for (auto const &[found, distance] : nearest) { auto const &line{m_lines[found.line]}; @@ -163,7 +173,9 @@ public: if ((found.index == 0 && along < -0.6) || (found.index + 1 == line.samples.size() && along > 0.6)) continue; auto const height{Point.y - (sample.position.y + sample.grade * along)}; - deepest = std::max(deepest, gauge::intrusion(Profile, line.sections[found.index], glm::dot(offset, normal(sample)), height)); + auto const depth{gauge::intrusion(Profile, line.sections[found.index], glm::dot(offset, normal(sample)), height)}; + if (depth > deepest.depth) + deepest = {depth, &line.route.spans[sample.span]}; } return deepest; } @@ -251,7 +263,7 @@ void scan_models(corridor const &Space, gauge::profile const &Profile, std::vect continue; if (false == Space.near(instance->location(), instance->radius())) continue; - editor_mode::gauge_hit hit{instance->name(), instance->location(), 0.0}; + 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 edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})}; @@ -260,14 +272,16 @@ void scan_models(corridor const &Space, gauge::profile const &Profile, std::vect 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 depth{Space.intrusion(Profile, point)}; - if (depth <= 0.0) + auto const found{Space.intrusion(Profile, point)}; + if (found.depth <= 0.0) continue; Triangles.insert(Triangles.end(), {A, B, C}); - if (depth > hit.depth) + if (found.depth > hit.depth) { - hit.depth = depth; + hit.depth = found.depth; hit.point = point; + hit.track = found.span->track; + hit.path = found.span->path; } return; } @@ -278,6 +292,46 @@ void scan_models(corridor const &Space, gauge::profile const &Profile, std::vect std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); } +// 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, gauge::profile const &Profile, std::vector &Surface, std::vector &Edges) +{ + auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { + auto const normal{corridor::normal(Sample)}; + return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); + }}; + // outline across the track, from the bottom right up and down to the bottom left + auto const ring{[&](std::size_t const Index) { + std::vector points; + for (int side : {-1, 1}) + for (std::size_t j = 0; j < Profile.outline.size(); ++j) + { + auto const &point{Profile.outline[side < 0 ? j : Profile.outline.size() - 1 - j]}; + auto const width{point.half_width + gauge::widening(Line.sections[Index], point.height, side)}; + points.push_back(lift(Line.samples[Index], side * width, point.height)); + } + 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; + } +} + double widest(gauge::profile const &Profile) { double result{0.0}; @@ -303,7 +357,7 @@ std::vector const &editor_mode::gauge_hits() const void editor_mode::update_gauge() { - if (ui()->mode() != nodebank_panel::TRACK) + if (false == m_gauge.open) { if (false == EditorOverlay.batches.empty()) EditorOverlay.clear(); @@ -340,50 +394,12 @@ void editor_mode::scan_gauge(TTrack &Track) gauge_load(); m_gauge.line = {}; auto const &profile{m_gauge.profiles[m_gauge.profile]}; - auto route{editor_track::run_route(Track, m_gauge.reach)}; + auto route{editor_track::run_route(Track, gauge_reach)}; std::vector const lines{make_line(route)}; - auto const &line{lines.front()}; - if (line.samples.size() < 2 || profile.outline.size() < 2) + if (lines.front().samples.size() < 2 || profile.outline.size() < 2) return; corridor const space{lines, widest(profile)}; - - auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { - auto const normal{corridor::normal(Sample)}; - return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); - }}; - // outline across the track, from the bottom right up and down to the bottom left - auto const ring{[&](std::size_t const Index) { - std::vector points; - for (int side : {-1, 1}) - for (std::size_t j = 0; j < profile.outline.size(); ++j) - { - auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; - auto const width{point.half_width + gauge::widening(line.sections[Index], point.height, side)}; - points.push_back(lift(line.samples[Index], side * width, point.height)); - } - return points; - }}; - auto &surface{m_gauge.line.surface}; - auto &edges{m_gauge.line.edges}; - 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; - } + build_tunnel(lines.front(), profile, m_gauge.line.surface, m_gauge.line.edges); scan_models(space, profile, m_gauge.line.intruding, m_gauge.line.hits); if (false == m_gauge.map.scanned) m_gauge.current = -1; @@ -433,7 +449,9 @@ void editor_mode::gauge_publish() batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, false, m_gauge.line.edges), batch({1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true, m_gauge.line.intruding), batch({1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true, m_gauge.map.intruding), - batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, false, marker)}; + batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, false, marker), + batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, false, m_gauge.spot.surface), + batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, false, m_gauge.spot.edges)}; } void editor_mode::gauge_focus(int const Index) @@ -443,6 +461,7 @@ void editor_mode::gauge_focus(int const Index) 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 target{hits[m_gauge.current].point}; // from the side the camera looks from, a bit above glm::dvec3 away{Camera.Pos.x - target.x, 0.0, Camera.Pos.z - target.z}; @@ -457,14 +476,38 @@ void editor_mode::gauge_focus(int const Index) m_focus_duration = 0.6; } -void editor_mode::render_gauge_ui() +void editor_mode::gauge_spot(gauge_hit const &Hit) { - if (false == ImGui::CollapsingHeader("Structure gauge")) + 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}); + auto const line{make_line(route)}; + if (line.samples.size() >= 2) + build_tunnel(line, m_gauge.profiles[m_gauge.profile], 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 along the line of the selected path,\nwith the triangles of the models which enter it in red.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II."); + ImGui::SetTooltip("Translucent tunnel of the gauge 500 m each way along the line of the selected path,\nwith the triangles of the models which enter it in red.\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(); auto const previous{m_gauge.profile}; for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) @@ -474,10 +517,6 @@ void editor_mode::render_gauge_ui() ImGui::RadioButton(m_gauge.profiles[i].name.c_str(), &m_gauge.profile, i); } bool changed{previous != m_gauge.profile}; - float reach{static_cast(m_gauge.reach)}; - if (ImGui::SliderFloat("Reach each way (m)", &reach, 50.0f, 2000.0f, "%.0f")) - m_gauge.reach = reach; - changed |= ImGui::IsItemDeactivatedAfterEdit(); if (ImGui::Button("Find the violations on the whole map")) scan_gauge_map(); @@ -499,6 +538,7 @@ void editor_mode::render_gauge_ui() changed |= render_gauge_outline(); if (changed) m_gauge.pending = true; + ImGui::End(); } void editor_mode::render_gauge_hits() diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 2fb4ace3..4e3b9a94 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -482,7 +482,6 @@ void editor_mode::render_track_window() m_infra.scope = 1; infra_recognize(); } - render_gauge_ui(); ImGui::End(); } diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index aaef5783..a4610491 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -143,6 +143,12 @@ void editor_ui::render_menu_contents() ImGui::MenuItem(STR_C("Include database"), nullptr, &m_includespanel.is_open); ImGui::EndMenu(); } + if (m_gaugewindow != nullptr) + { + ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gaugewindow); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks (skrajnia budowli)")); + } } void editor_ui::set_file_actions(std::function Save, std::function Export) diff --git a/application/editoruilayer.h b/application/editoruilayer.h index f4b3c971..7ae7faa9 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -59,6 +59,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 @@ -84,4 +89,5 @@ class editor_ui : public ui_layer std::function m_trackoptions; std::function m_gizmooptions; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any + bool *m_gaugewindow{nullptr}; }; From e4e3aad853b543eacf732aa76e4805a3f6f53a8a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:00:43 +0200 Subject: [PATCH 25/34] Structure gauge: G1/G2/GA/GB/GC installation gauges, road clearance, Toolset button --- application/editormode.h | 8 +- application/editormodegauge.cpp | 192 ++++++++++++++++++++++---------- application/editoruilayer.cpp | 18 ++- editor/editorGauge.cpp | 52 +++++++-- editor/editorGauge.hpp | 20 +++- 5 files changed, 212 insertions(+), 78 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 4bef2f71..8114831f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -321,7 +321,8 @@ class editor_mode : public application_mode, private editor_track::observer bool open{false}; // window bool enabled{true}; // tunnel along the selected path std::vector profiles; - int profile{0}; + 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 @@ -349,6 +350,8 @@ class editor_mode : public application_mode, private editor_track::observer bool published{false}; // overlay up to date } m_gauge; void gauge_load(); + // the chosen railway and road outlines, null when there's none of the kind + std::pair gauge_profiles() const; std::vector const &gauge_hits() const; void update_gauge(); void scan_gauge(TTrack &Track); @@ -358,7 +361,8 @@ class editor_mode : public application_mode, private editor_track::observer void gauge_spot(gauge_hit const &Hit); void render_gauge_window(); void render_gauge_hits(); - bool render_gauge_outline(); + bool render_gauge_choice(char const *Label, int &Index, bool Road); + bool render_gauge_outline(int Index); struct route_design { TTrack *from{nullptr}; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 2a07a950..d08b91ba 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -45,37 +45,71 @@ double const gauge_spot_reach{60.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; }; -gauge_line make_line(editor_track::route &Route) +// the outline checked along the track: the railway one or the road one +struct gauge_choice +{ + gauge::profile const *railway{nullptr}; + gauge::profile const *road{nullptr}; + + gauge::profile const *of(TTrack const &Track) const + { + if ((Track.iCategoryFlag & 1) != 0) + return railway; + if ((Track.iCategoryFlag & 2) != 0) + return road; + return nullptr; + } +}; + +// 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) { - sample.position.y += gauge::rail_height; + 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(chainage, curvature, 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; } // lines through all paths of the scenery, each path in one of them at least -std::vector scenery_lines() +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; }}; for (auto *track : simulation::Paths.sequence()) { - if (track == nullptr || visited.count(track) > 0 || false == editor_track::is_supported(*track)) + if (track == nullptr || visited.count(track) > 0 || false == editor_track::is_supported(*track) || Choice.of(*track) == nullptr) 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 @@ -86,15 +120,15 @@ std::vector scenery_lines() spans.pop_back(); for (auto const &span : spans) visited.insert(span.track); - lines.push_back(make_line(route)); + lines.push_back(make_line(route, *Choice.of(*track))); } // diverging paths of the switches for (auto *track : simulation::Paths.sequence()) - if (track != nullptr && track->eType == tt_Switch && editor_track::is_supported(*track)) + if (track != nullptr && track->eType == tt_Switch && editor_track::is_supported(*track) && Choice.of(*track) != nullptr) { editor_track::route route; route.spans.push_back({track, 1, true}); - lines.push_back(make_line(route)); + lines.push_back(make_line(route, *Choice.of(*track))); } return lines; } @@ -103,11 +137,12 @@ std::vector scenery_lines() class corridor { public: - corridor(std::vector const &Lines, double const Margin) : m_lines{Lines}, m_margin{Margin} + 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) { + m_margin = std::max(m_margin, reach(*Lines[line].profile) + Lines[line].sections[i].base); 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)); @@ -139,7 +174,7 @@ public: editor_track::route_span const *span{nullptr}; // of the line the point enters }; // deepest intrusion of the point into the gauge of the lines nearby - hit intrusion(gauge::profile const &Profile, glm::dvec3 const &Point) const + hit intrusion(glm::dvec3 const &Point) const { auto const point{plan_of(Point)}; auto const center{index(point, cell)}; @@ -173,7 +208,7 @@ public: if ((found.index == 0 && along < -0.6) || (found.index + 1 == line.samples.size() && along > 0.6)) continue; auto const height{Point.y - (sample.position.y + sample.grade * along)}; - auto const depth{gauge::intrusion(Profile, line.sections[found.index], glm::dot(offset, normal(sample)), height)}; + auto const depth{gauge::intrusion(*line.profile, line.sections[found.index], glm::dot(offset, normal(sample)), height)}; if (depth > deepest.depth) deepest = {depth, &line.route.spans[sample.span]}; } @@ -208,8 +243,17 @@ private: return m_lines[Entry.line].samples[Entry.index]; } + // half-width of the outline with the room for the widening in the sharpest curves on the largest cant + static double reach(gauge::profile const &Profile) + { + double result{0.0}; + for (auto const &point : Profile.outline) + result = std::max(result, point.half_width); + return result + 1.2; + } + std::vector const &m_lines; - double m_margin; + double m_margin{0.0}; std::unordered_map> m_cells; std::unordered_set m_regions; }; @@ -253,7 +297,7 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } // triangles of the models entering the gauge, and the deepest place of each model, the deepest models first -void scan_models(corridor const &Space, gauge::profile const &Profile, std::vector &Triangles, std::vector &Hits) +void scan_models(corridor const &Space, std::vector &Triangles, std::vector &Hits) { for (auto *instance : simulation::Instances.sequence()) { @@ -272,7 +316,7 @@ void scan_models(corridor const &Space, gauge::profile const &Profile, std::vect 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(Profile, point)}; + auto const found{Space.intrusion(point)}; if (found.depth <= 0.0) continue; Triangles.insert(Triangles.end(), {A, B, C}); @@ -293,8 +337,9 @@ void scan_models(corridor const &Space, gauge::profile const &Profile, std::vect } // 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, gauge::profile const &Profile, std::vector &Surface, std::vector &Edges) +void build_tunnel(gauge_line const &Line, std::vector &Surface, std::vector &Edges) { + auto const &profile{*Line.profile}; auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { auto const normal{corridor::normal(Sample)}; return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); @@ -303,10 +348,10 @@ void build_tunnel(gauge_line const &Line, gauge::profile const &Profile, std::ve auto const ring{[&](std::size_t const Index) { std::vector points; for (int side : {-1, 1}) - for (std::size_t j = 0; j < Profile.outline.size(); ++j) + for (std::size_t j = 0; j < profile.outline.size(); ++j) { - auto const &point{Profile.outline[side < 0 ? j : Profile.outline.size() - 1 - j]}; - auto const width{point.half_width + gauge::widening(Line.sections[Index], point.height, side)}; + auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; + auto const width{Line.sections[Index].base + point.half_width + gauge::widening(Line.sections[Index], point.height, side)}; points.push_back(lift(Line.samples[Index], side * width, point.height)); } return points; @@ -332,22 +377,30 @@ void build_tunnel(gauge_line const &Line, gauge::profile const &Profile, std::ve } } -double widest(gauge::profile const &Profile) -{ - double result{0.0}; - for (auto const &point : Profile.outline) - result = std::max(result, point.half_width); - // with the room for the widening in the sharpest curves on the largest cant - return result + 1.2; -} - } // namespace void editor_mode::gauge_load() { if (m_gauge.profiles.empty()) m_gauge.profiles = gauge::load_profiles(gauge_file); - m_gauge.profile = std::clamp(m_gauge.profile, 0, static_cast(m_gauge.profiles.size()) - 1); + // 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; + } +} + +std::pair editor_mode::gauge_profiles() 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 @@ -393,14 +446,17 @@ void editor_mode::scan_gauge(TTrack &Track) { gauge_load(); m_gauge.line = {}; - auto const &profile{m_gauge.profiles[m_gauge.profile]}; - auto route{editor_track::run_route(Track, gauge_reach)}; - std::vector const lines{make_line(route)}; - if (lines.front().samples.size() < 2 || profile.outline.size() < 2) + auto const [railway, road]{gauge_profiles()}; + auto const *profile{gauge_choice{railway, road}.of(Track)}; + if (profile == nullptr || profile->outline.size() < 2) return; - corridor const space{lines, widest(profile)}; - build_tunnel(lines.front(), profile, m_gauge.line.surface, m_gauge.line.edges); - scan_models(space, profile, m_gauge.line.intruding, m_gauge.line.hits); + auto route{editor_track::run_route(Track, gauge_reach)}; + std::vector const lines{make_line(route, *profile)}; + if (lines.front().samples.size() < 2) + return; + corridor const space{lines}; + build_tunnel(lines.front(), m_gauge.line.surface, m_gauge.line.edges); + scan_models(space, m_gauge.line.intruding, m_gauge.line.hits); if (false == m_gauge.map.scanned) m_gauge.current = -1; } @@ -410,16 +466,16 @@ void editor_mode::scan_gauge_map() gauge_load(); auto const started{std::chrono::steady_clock::now()}; m_gauge.map = {}; - auto const &profile{m_gauge.profiles[m_gauge.profile]}; - auto const lines{scenery_lines()}; - corridor const space{lines, widest(profile)}; - scan_models(space, profile, m_gauge.map.intruding, m_gauge.map.hits); + auto const [railway, road]{gauge_profiles()}; + auto const lines{scenery_lines({railway, road})}; + corridor const space{lines}; + scan_models(space, m_gauge.map.intruding, m_gauge.map.hits); m_gauge.map.scanned = true; m_gauge.map.history = m_history.size(); - m_gauge.map.profile = profile.name; + m_gauge.map.profile = (railway ? railway->name : std::string{"-"}) + ", " + (road ? road->name : std::string{"-"}); m_gauge.current = -1; m_gauge.published = false; - WriteLog("Structure gauge (" + profile.name + "): " + std::to_string(m_gauge.map.hits.size()) + " models enter it along " + std::to_string(lines.size()) + " lines, scanned in " + + WriteLog("Structure gauge (" + m_gauge.map.profile + "): " + std::to_string(m_gauge.map.hits.size()) + " models enter it along " + std::to_string(lines.size()) + " lines, scanned in " + std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()) + " ms"); if (false == m_gauge.map.hits.empty()) gauge_focus(0); @@ -489,9 +545,13 @@ void editor_mode::gauge_spot(gauge_hit const &Hit) route = editor_track::run_route(*track, gauge_spot_reach); else route.spans.push_back({track, Hit.path, true}); - auto const line{make_line(route)}; + auto const [railway, road]{gauge_profiles()}; + auto const *profile{gauge_choice{railway, road}.of(*track)}; + if (profile == nullptr) + return; + auto const line{make_line(route, *profile)}; if (line.samples.size() >= 2) - build_tunnel(line, m_gauge.profiles[m_gauge.profile], m_gauge.spot.surface, m_gauge.spot.edges); + build_tunnel(line, m_gauge.spot.surface, m_gauge.spot.edges); } void editor_mode::render_gauge_window() @@ -509,14 +569,12 @@ void editor_mode::render_gauge_window() if (ImGui::IsItemHovered()) ImGui::SetTooltip("Translucent tunnel of the gauge 500 m each way along the line of the selected path,\nwith the triangles of the models which enter it in red.\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(); - auto const previous{m_gauge.profile}; - for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) - { - if (i % 2 == 1) - ImGui::SameLine(160.0f); - ImGui::RadioButton(m_gauge.profiles[i].name.c_str(), &m_gauge.profile, i); - } - bool changed{previous != m_gauge.profile}; + 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 (ImGui::Button("Find the violations on the whole map")) scan_gauge_map(); @@ -535,7 +593,8 @@ void editor_mode::render_gauge_window() 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(); + changed |= render_gauge_outline(m_gauge.profile); + changed |= render_gauge_outline(m_gauge.road); if (changed) m_gauge.pending = true; ImGui::End(); @@ -572,12 +631,32 @@ void editor_mode::render_gauge_hits() ImGui::EndChild(); } -bool editor_mode::render_gauge_outline() +bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road) { - if (false == ImGui::TreeNode("Outline (right half)")) + 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; - ImGui::TextDisabled("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); - auto &profile{m_gauge.profiles[m_gauge.profile]}; + 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{false}; bool inserted{false}; int remove{-1}; @@ -626,6 +705,7 @@ bool editor_mode::render_gauge_outline() if (ImGui::SmallButton("Restore the starting outlines")) { m_gauge.profiles = gauge::default_profiles(); + gauge_load(); edited = true; } if (edited) diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index a4610491..a4c10e01 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -96,6 +96,18 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) void editor_ui::render_header_sections() { + 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 (ImGui::CollapsingHeader("Gizmo", ImGuiTreeNodeFlags_DefaultOpen)) { if (m_gizmooptions) @@ -143,12 +155,6 @@ void editor_ui::render_menu_contents() ImGui::MenuItem(STR_C("Include database"), nullptr, &m_includespanel.is_open); ImGui::EndMenu(); } - if (m_gaugewindow != nullptr) - { - ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gaugewindow); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks (skrajnia budowli)")); - } } void editor_ui::set_file_actions(std::function Save, std::function Export) diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp index 59acae31..9ba12e9b 100644 --- a/editor/editorGauge.cpp +++ b/editor/editorGauge.cpp @@ -87,6 +87,18 @@ private: std::vector m_integral; }; +// 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, {{1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}}}; + for (auto point = Points.rbegin(); point != Points.rend(); ++point) + result.outline.push_back({point->second * 0.001, point->first * 0.001}); + return result; +} + } // namespace std::vector default_profiles() @@ -97,16 +109,31 @@ std::vector default_profiles() 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, lower}; + profile result{Name, kind::unified, lower}; for (auto const &part : Parts) result.outline.insert(result.outline.end(), part.begin(), part.end()); return result; }}; - 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})}; + // 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, then pairs of half-width and height +// 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}; @@ -116,8 +143,13 @@ std::vector load_profiles(std::string const &File) { std::istringstream stream{line}; profile profile; - if (false == static_cast(stream >> std::quoted(profile.name))) + 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); @@ -132,18 +164,18 @@ 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); + 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(std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant) +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) + 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) @@ -155,7 +187,7 @@ std::vector
sections(std::vector const &Chainage, std::vector 0.0) == (side == 1)}; - auto const upper{upper_widening(radius)}; + 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; @@ -195,7 +227,7 @@ double half_width(profile const &Profile, section const &Section, double const H } if (width < 0.0) return width; - return width + widening(Section, Height, Side); + return Section.base + width + widening(Section, Height, Side); } double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp index ee9b6be8..d7e94cf9 100644 --- a/editor/editorGauge.hpp +++ b/editor/editorGauge.hpp @@ -18,11 +18,21 @@ http://mozilla.org/MPL/2.0/. namespace gauge { -// right half of the outline on a straight track, in metres: half-width from the axis and height above the rail top, -// listed from the bottom up; the left half is the mirror +// 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}; @@ -31,7 +41,8 @@ struct profile std::vector outline; }; -// GPL-1 and GPL-2 over the limit installation gauge below 1170 mm, with and without the pantograph gauge +// 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); @@ -46,13 +57,14 @@ 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 }; // 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 -std::vector
sections(std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); +std::vector
sections(kind Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); // widening of the side (+1 left, -1 right) at the height double widening(section const &Section, double Height, int Side); From 0b5b69d76e47730210e319c5cf40224067bf1c7b Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:05:31 +0200 Subject: [PATCH 26/34] Structure gauge button in the Track tab of the toolset --- application/editoruilayer.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index a4c10e01..b5484724 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; @@ -96,18 +108,6 @@ void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) void editor_ui::render_header_sections() { - 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 (ImGui::CollapsingHeader("Gizmo", ImGuiTreeNodeFlags_DefaultOpen)) { if (m_gizmooptions) From 7e78ca8714a7c31b5060fef611e953bbb5a9ba07 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:12:14 +0200 Subject: [PATCH 27/34] Structure gauge: the lower part turns with the plane of the rail heads on cant --- application/editormodegauge.cpp | 23 +++++++++++++++++++--- editor/editorGauge.cpp | 34 ++++++++++++++++++++++++++++++--- editor/editorGauge.hpp | 12 +++++++++++- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index d08b91ba..2d6c7b41 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -95,6 +95,24 @@ gauge_line make_line(editor_track::route &Route, gauge::profile const &Profile) cant.push_back(sample.cant); } line.sections = gauge::sections(Profile.kind, chainage, curvature, cant); + if (false == road) + { + // the outer rail is raised: the side from the nearest curve along the line + std::vector sides(curvature.size(), 0.0); + double side{0.0}; + for (std::size_t i = 0; i < curvature.size(); ++i) + sides[i] = side = std::abs(curvature[i]) > 1e-6 ? (curvature[i] > 0.0 ? 1.0 : -1.0) : side; + side = 0.0; + for (std::size_t i = curvature.size(); i-- > 0;) + { + if (std::abs(curvature[i]) > 1e-6) + side = curvature[i] > 0.0 ? 1.0 : -1.0; + else if (sides[i] == 0.0) + sides[i] = side; + } + for (std::size_t i = 0; i < curvature.size(); ++i) + line.sections[i].tilt = -sides[i] * std::min(1.0, cant[i] / 1.5); + } 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); @@ -350,9 +368,8 @@ void build_tunnel(gauge_line const &Line, std::vector &Surface, std: for (int side : {-1, 1}) for (std::size_t j = 0; j < profile.outline.size(); ++j) { - auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; - auto const width{Line.sections[Index].base + point.half_width + gauge::widening(Line.sections[Index], point.height, side)}; - points.push_back(lift(Line.samples[Index], side * width, point.height)); + auto const point{gauge::outline_point(profile, Line.sections[Index], side < 0 ? j : profile.outline.size() - 1 - j, side)}; + points.push_back(lift(Line.samples[Index], point.lateral, point.height)); } return points; }}; diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp index 9ba12e9b..8674521f 100644 --- a/editor/editorGauge.cpp +++ b/editor/editorGauge.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorGauge.hpp" +#include #include #include @@ -87,6 +88,18 @@ private: 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}; +} + // names of the kinds in the file char const *const kind_names[]{"unified", "installation", "road"}; @@ -210,7 +223,18 @@ std::vector
sections(kind const Kind, std::vector const &Chaina double widening(section const &Section, double const Height, int const Side) { auto const i{Side > 0 ? 1 : 0}; - return (Height < lower_part ? Section.lower[i] : Section.upper[i]) + Section.cant[i] * Height / 1.5; + return Height < lower_part ? Section.lower[i] : Section.upper[i] + Section.cant[i] * Height / 1.5; +} + +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 width{Section.base + point.half_width + (lower ? Section.lower[i] : Section.upper[i] + Section.cant[i] * point.height / 1.5)}; + place const result{Side * width, point.height}; + return lower && Section.tilt != 0.0 ? turn(Section, result, Section.tilt) : result; } double half_width(profile const &Profile, section const &Section, double const Height, int const Side) @@ -232,10 +256,14 @@ double half_width(profile const &Profile, section const &Section, double const H double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) { - auto const width{half_width(Profile, Section, Height, Lateral >= 0.0 ? 1 : -1)}; + // the point in the plane of the rail heads, for the lower part + auto const local{Section.tilt != 0.0 ? turn(Section, {Lateral, Height}, -Section.tilt) : place{Lateral, Height}}; + auto const lower{local.height < lower_part}; + auto const point{lower ? local : place{Lateral, std::max(Height, lower_part)}}; + auto const width{half_width(Profile, Section, point.height, point.lateral >= 0.0 ? 1 : -1)}; if (width < 0.0) return 0.0; - return width - std::abs(Lateral); + return width - std::abs(point.lateral); } } // namespace gauge diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp index d7e94cf9..609462ff 100644 --- a/editor/editorGauge.hpp +++ b/editor/editorGauge.hpp @@ -61,13 +61,23 @@ struct section 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 std::vector
sections(kind Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); -// widening of the side (+1 left, -1 right) at the height +// widening of the side (+1 left, -1 right) at the height; the cant shifts only the part above the lower one, +// the lower part turns with the plane of the rail heads instead double widening(section const &Section, double Height, int Side); +// point of the outline on the side at the section, with the widening and the turn of the lower part +place outline_point(profile const &Profile, section const &Section, std::size_t Index, int Side); // half-width of the gauge at the height on the side, negative above or below the outline double half_width(profile const &Profile, section const &Section, double Height, int Side); // depth of the point inside the gauge, metres, not positive outside of it. Lateral positive to the left From ae8a046f79cc704e9b3e330f7e37f84fcffd9c06 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:17:50 +0200 Subject: [PATCH 28/34] Structure gauge: wheel zone in the common lower part, depth as the distance from the outline --- editor/editorGauge.cpp | 73 +++++++++++++++++++++++------------------- editor/editorGauge.hpp | 11 +++---- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp index 8674521f..c23b3149 100644 --- a/editor/editorGauge.cpp +++ b/editor/editorGauge.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include #include namespace gauge @@ -100,13 +101,25 @@ place turn(section const &Section, place const &Point, double const Sine) 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, {{1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}}}; + 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; @@ -117,7 +130,8 @@ profile installation(std::string const &Name, std::vector> c std::vector default_profiles() { // limit installation gauge up to 1170 mm, then the outline of the unified gauge - std::vector const lower{{1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}, {1.675, 1.170}, {2.000, 1.170}}; + 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}}; @@ -220,50 +234,43 @@ std::vector
sections(kind const Kind, std::vector const &Chaina return result; } -double widening(section const &Section, double const Height, int const Side) -{ - auto const i{Side > 0 ? 1 : 0}; - return Height < lower_part ? Section.lower[i] : Section.upper[i] + Section.cant[i] * Height / 1.5; -} - 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 width{Section.base + point.half_width + (lower ? Section.lower[i] : Section.upper[i] + Section.cant[i] * point.height / 1.5)}; + 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; } -double half_width(profile const &Profile, section const &Section, double const Height, int const Side) -{ - double width{-1.0}; - for (std::size_t i = 1; i < Profile.outline.size(); ++i) - { - auto const &a{Profile.outline[i - 1]}; - auto const &b{Profile.outline[i]}; - if (Height < std::min(a.height, b.height) || Height > std::max(a.height, b.height)) - continue; - auto const span{b.height - a.height}; - width = std::max(width, std::abs(span) < 1e-9 ? std::max(a.half_width, b.half_width) : a.half_width + (b.half_width - a.half_width) * (Height - a.height) / span); - } - if (width < 0.0) - return width; - return Section.base + width + widening(Section, Height, Side); -} - double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) { - // the point in the plane of the rail heads, for the lower part - auto const local{Section.tilt != 0.0 ? turn(Section, {Lateral, Height}, -Section.tilt) : place{Lateral, Height}}; - auto const lower{local.height < lower_part}; - auto const point{lower ? local : place{Lateral, std::max(Height, lower_part)}}; - auto const width{half_width(Profile, Section, point.height, point.lateral >= 0.0 ? 1 : -1)}; - if (width < 0.0) + auto const count{Profile.outline.size()}; + if (count < 2) return 0.0; - return width - std::abs(point.lateral); + // the outline from the bottom right up and down to the bottom left, closed across the bottom + auto const vertex{[&](std::size_t const Index) { + return Index < count ? outline_point(Profile, Section, Index, -1) : outline_point(Profile, Section, 2 * count - 1 - Index, 1); + }}; + bool inside{false}; + auto nearest{std::numeric_limits::max()}; + auto previous{vertex(2 * count - 1)}; + for (std::size_t i = 0; i < 2 * count; ++i) + { + auto const current{vertex(i)}; + auto const lateral{current.lateral - previous.lateral}; + auto const height{current.height - previous.height}; + if ((current.height > Height) != (previous.height > Height) && Lateral < previous.lateral + lateral * (Height - previous.height) / height) + inside = false == inside; + auto const length{lateral * lateral + height * height}; + auto const t{length > 0.0 ? std::clamp(((Lateral - previous.lateral) * lateral + (Height - previous.height) * height) / length, 0.0, 1.0) : 0.0}; + nearest = std::min(nearest, std::hypot(Lateral - previous.lateral - lateral * t, Height - previous.height - height * t)); + previous = current; + } + return inside ? nearest : -nearest; } } // namespace gauge diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp index 609462ff..f551f248 100644 --- a/editor/editorGauge.hpp +++ b/editor/editorGauge.hpp @@ -73,14 +73,11 @@ struct place // The widening begins and ends ahead of the changes of the curvature and cant, as the vehicles enter a curve std::vector
sections(kind Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); -// widening of the side (+1 left, -1 right) at the height; the cant shifts only the part above the lower one, -// the lower part turns with the plane of the rail heads instead -double widening(section const &Section, double Height, int Side); -// point of the outline on the side at the section, with the widening and the turn of the lower part +// point of the outline on the side (+1 left, -1 right) at the section, with the widening: the cant shifts the part +// above the lower one, the lower part turns with the plane of the rail heads instead place outline_point(profile const &Profile, section const &Section, std::size_t Index, int Side); -// half-width of the gauge at the height on the side, negative above or below the outline -double half_width(profile const &Profile, section const &Section, double Height, int Side); -// depth of the point inside the gauge, metres, not positive outside of it. Lateral positive to the left +// distance of the point from the outline in any direction, metres, positive inside and negative outside. +// Lateral positive to the left double intrusion(profile const &Profile, section const &Section, double Lateral, double Height); } // namespace gauge From ce0c311106a120ca9dd6a7b0ecce9d3c43c26551 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:22:57 +0200 Subject: [PATCH 29/34] Structure gauge refactor: outline ring, tilt in the sections, outline choice, shared track line --- application/editormode.h | 4 +- application/editormodegauge.cpp | 217 ++++++++++++++------------------ editor/editorGauge.cpp | 98 +++++++++++---- editor/editorGauge.hpp | 26 ++-- 4 files changed, 184 insertions(+), 161 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 8114831f..ae52afd9 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -350,8 +350,8 @@ class editor_mode : public application_mode, private editor_track::observer bool published{false}; // overlay up to date } m_gauge; void gauge_load(); - // the chosen railway and road outlines, null when there's none of the kind - std::pair gauge_profiles() const; + // 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); diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 2d6c7b41..f43ec42e 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -26,6 +26,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -51,22 +52,6 @@ struct gauge_line std::vector sections; }; -// the outline checked along the track: the railway one or the road one -struct gauge_choice -{ - gauge::profile const *railway{nullptr}; - gauge::profile const *road{nullptr}; - - gauge::profile const *of(TTrack const &Track) const - { - if ((Track.iCategoryFlag & 1) != 0) - return railway; - if ((Track.iCategoryFlag & 2) != 0) - return road; - return nullptr; - } -}; - // 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) { @@ -95,39 +80,34 @@ gauge_line make_line(editor_track::route &Route, gauge::profile const &Profile) cant.push_back(sample.cant); } line.sections = gauge::sections(Profile.kind, chainage, curvature, cant); - if (false == road) - { - // the outer rail is raised: the side from the nearest curve along the line - std::vector sides(curvature.size(), 0.0); - double side{0.0}; - for (std::size_t i = 0; i < curvature.size(); ++i) - sides[i] = side = std::abs(curvature[i]) > 1e-6 ? (curvature[i] > 0.0 ? 1.0 : -1.0) : side; - side = 0.0; - for (std::size_t i = curvature.size(); i-- > 0;) - { - if (std::abs(curvature[i]) > 1e-6) - side = curvature[i] > 0.0 ? 1.0 : -1.0; - else if (sides[i] == 0.0) - sides[i] = side; - } - for (std::size_t i = 0; i < curvature.size(); ++i) - line.sections[i].tilt = -sides[i] * std::min(1.0, cant[i] / 1.5); - } 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; } +// 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{Choice.of(Track.iCategoryFlag)}; + if (profile == nullptr || profile->outline.size() < 2) + 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 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) && Choice.of(Track->iCategoryFlag) != nullptr; }}; for (auto *track : simulation::Paths.sequence()) { - if (track == nullptr || visited.count(track) > 0 || false == editor_track::is_supported(*track) || Choice.of(*track) == nullptr) + 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 @@ -138,15 +118,15 @@ std::vector scenery_lines(gauge_choice const &Choice) spans.pop_back(); for (auto const &span : spans) visited.insert(span.track); - lines.push_back(make_line(route, *Choice.of(*track))); + lines.push_back(make_line(route, *Choice.of(track->iCategoryFlag))); } // diverging paths of the switches for (auto *track : simulation::Paths.sequence()) - if (track != nullptr && track->eType == tt_Switch && editor_track::is_supported(*track) && Choice.of(*track) != nullptr) + if (checked(track) && track->eType == tt_Switch) { editor_track::route route; route.spans.push_back({track, 1, true}); - lines.push_back(make_line(route, *Choice.of(*track))); + lines.push_back(make_line(route, *Choice.of(track->iCategoryFlag))); } return lines; } @@ -160,7 +140,8 @@ public: for (std::uint32_t line = 0; line < Lines.size(); ++line) for (std::uint32_t i = 0; i < Lines[line].samples.size(); ++i) { - m_margin = std::max(m_margin, reach(*Lines[line].profile) + Lines[line].sections[i].base); + // 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)); @@ -225,8 +206,8 @@ public: 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; - auto const height{Point.y - (sample.position.y + sample.grade * along)}; - auto const depth{gauge::intrusion(*line.profile, line.sections[found.index], glm::dot(offset, normal(sample)), height)}; + 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]}; } @@ -261,17 +242,9 @@ private: return m_lines[Entry.line].samples[Entry.index]; } - // half-width of the outline with the room for the widening in the sharpest curves on the largest cant - static double reach(gauge::profile const &Profile) - { - double result{0.0}; - for (auto const &point : Profile.outline) - result = std::max(result, point.half_width); - return result + 1.2; - } - 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; }; @@ -357,20 +330,14 @@ void scan_models(corridor const &Space, std::vector &Triangles, std: // 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) { - auto const &profile{*Line.profile}; - auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { - auto const normal{corridor::normal(Sample)}; - return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); - }}; - // outline across the track, from the bottom right up and down to the bottom left + 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 (int side : {-1, 1}) - for (std::size_t j = 0; j < profile.outline.size(); ++j) - { - auto const point{gauge::outline_point(profile, Line.sections[Index], side < 0 ? j : profile.outline.size() - 1 - j, side)}; - points.push_back(lift(Line.samples[Index], point.lateral, point.height)); - } + 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; @@ -394,6 +361,57 @@ void build_tunnel(gauge_line const &Line, std::vector &Surface, std: } } +// 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 void editor_mode::gauge_load() @@ -414,7 +432,7 @@ void editor_mode::gauge_load() } } -std::pair editor_mode::gauge_profiles() const +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)}; @@ -463,14 +481,11 @@ void editor_mode::scan_gauge(TTrack &Track) { gauge_load(); m_gauge.line = {}; - auto const [railway, road]{gauge_profiles()}; - auto const *profile{gauge_choice{railway, road}.of(Track)}; - if (profile == nullptr || profile->outline.size() < 2) - return; auto route{editor_track::run_route(Track, gauge_reach)}; - std::vector const lines{make_line(route, *profile)}; - if (lines.front().samples.size() < 2) + 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.intruding, m_gauge.line.hits); @@ -483,13 +498,14 @@ void editor_mode::scan_gauge_map() gauge_load(); auto const started{std::chrono::steady_clock::now()}; m_gauge.map = {}; - auto const [railway, road]{gauge_profiles()}; - auto const lines{scenery_lines({railway, road})}; + auto const choice{gauge_choice()}; + auto const lines{scenery_lines(choice)}; corridor const space{lines}; scan_models(space, m_gauge.map.intruding, m_gauge.map.hits); m_gauge.map.scanned = true; m_gauge.map.history = m_history.size(); - m_gauge.map.profile = (railway ? railway->name : std::string{"-"}) + ", " + (road ? road->name : std::string{"-"}); + auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; + m_gauge.map.profile = name(choice.railway) + ", " + name(choice.road); m_gauge.current = -1; m_gauge.published = false; WriteLog("Structure gauge (" + m_gauge.map.profile + "): " + std::to_string(m_gauge.map.hits.size()) + " models enter it along " + std::to_string(lines.size()) + " lines, scanned in " + @@ -562,13 +578,8 @@ void editor_mode::gauge_spot(gauge_hit const &Hit) route = editor_track::run_route(*track, gauge_spot_reach); else route.spans.push_back({track, Hit.path, true}); - auto const [railway, road]{gauge_profiles()}; - auto const *profile{gauge_choice{railway, road}.of(*track)}; - if (profile == nullptr) - return; - auto const line{make_line(route, *profile)}; - if (line.samples.size() >= 2) - build_tunnel(line, m_gauge.spot.surface, m_gauge.spot.edges); + 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() @@ -674,51 +685,7 @@ bool editor_mode::render_gauge_outline(int const Index) 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{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(profile.outline.size()); ++i) - { - ImGui::PushID(i); - auto &point{profile.outline[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{profile.outline[std::min(i + 1, profile.outline.size() - 1)]}; - profile.outline.insert(profile.outline.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 (profile.outline.size() > 2 && ImGui::SmallButton("x")) - remove = i; - ImGui::NextColumn(); - ImGui::PopID(); - if (inserted) - break; - } - ImGui::Columns(1); - if (remove >= 0) - { - profile.outline.erase(profile.outline.begin() + remove); - edited = true; - } + bool edited{edit_points(profile.outline)}; if (ImGui::SmallButton("Restore the starting outlines")) { m_gauge.profiles = gauge::default_profiles(); diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp index c23b3149..bb78f25a 100644 --- a/editor/editorGauge.cpp +++ b/editor/editorGauge.cpp @@ -125,8 +125,49 @@ profile installation(std::string const &Name, std::vector> c 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 @@ -231,46 +272,49 @@ std::vector
sections(kind const Kind, std::vector const &Chaina 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; } -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; -} - -double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) +void ring(profile const &Profile, section const &Section, std::vector &Points) { auto const count{Profile.outline.size()}; - if (count < 2) + 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; - // the outline from the bottom right up and down to the bottom left, closed across the bottom - auto const vertex{[&](std::size_t const Index) { - return Index < count ? outline_point(Profile, Section, Index, -1) : outline_point(Profile, Section, 2 * count - 1 - Index, 1); - }}; bool inside{false}; auto nearest{std::numeric_limits::max()}; - auto previous{vertex(2 * count - 1)}; - for (std::size_t i = 0; i < 2 * count; ++i) + auto const *previous{&Ring.back()}; + for (auto const ¤t : Ring) { - auto const current{vertex(i)}; - auto const lateral{current.lateral - previous.lateral}; - auto const height{current.height - previous.height}; - if ((current.height > Height) != (previous.height > Height) && Lateral < previous.lateral + lateral * (Height - previous.height) / height) + 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(((Lateral - previous.lateral) * lateral + (Height - previous.height) * height) / length, 0.0, 1.0) : 0.0}; - nearest = std::min(nearest, std::hypot(Lateral - previous.lateral - lateral * t, Height - previous.height - height * t)); - previous = current; + 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 index f551f248..caae3a4c 100644 --- a/editor/editorGauge.hpp +++ b/editor/editorGauge.hpp @@ -41,6 +41,15 @@ struct profile 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(); @@ -70,14 +79,17 @@ struct place 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 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); -// point of the outline on the side (+1 left, -1 right) at the section, with the widening: the cant shifts the part -// above the lower one, the lower part turns with the plane of the rail heads instead -place outline_point(profile const &Profile, section const &Section, std::size_t Index, int Side); -// distance of the point from the outline in any direction, metres, positive inside and negative outside. -// Lateral positive to the left -double intrusion(profile const &Profile, section const &Section, double Lateral, double Height); +// 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 From 1ab5e1e8c368e3f829ac6a81b219bd60970a9341 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:25:12 +0200 Subject: [PATCH 30/34] Structure gauge: camera along the track at the height of the hit, close to it --- application/editormode.h | 1 + application/editormodegauge.cpp | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index ae52afd9..7b32ff0f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -47,6 +47,7 @@ class editor_mode : public application_mode, private editor_track::observer 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(); diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index f43ec42e..7c0ade95 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -42,6 +42,8 @@ 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}; +// 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 @@ -171,6 +173,7 @@ public: { 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 @@ -209,7 +212,7 @@ public: 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]}; + deepest = {depth, &line.route.spans[sample.span], sample.direction}; } return deepest; } @@ -317,6 +320,7 @@ void scan_models(corridor const &Space, std::vector &Triangles, std: hit.point = point; hit.track = found.span->track; hit.path = found.span->path; + hit.direction = found.direction; } return; } @@ -551,14 +555,15 @@ void editor_mode::gauge_focus(int const Index) 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 target{hits[m_gauge.current].point}; - // from the side the camera looks from, a bit above - glm::dvec3 away{Camera.Pos.x - target.x, 0.0, Camera.Pos.z - target.z}; - away = glm::length(away) > 1e-3 ? glm::normalize(away) : glm::dvec3(1.0, 0.0, 0.0); + 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 = target + away * 12.0 + glm::dvec3(0.0, 4.0, 0.0); - auto const look{glm::normalize(target - m_focus_target_pos)}; + 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; From e69601dfa026b1477062efb101b61b3e021d00a3 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:27:52 +0200 Subject: [PATCH 31/34] Structure gauge: no red highlight of the intruding triangles --- application/editormode.h | 2 -- application/editormodegauge.cpp | 32 +++++++++++++------------------- rendering/editoroverlay.h | 1 - rendering/opengl33renderer.cpp | 6 ------ 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 7b32ff0f..dd4e4872 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -331,7 +331,6 @@ class editor_mode : public application_mode, private editor_track::observer { std::vector surface; // triangles of the tunnel std::vector edges; // lines - std::vector intruding; // triangles of the models std::vector hits; } line; // along the selected path struct @@ -339,7 +338,6 @@ class editor_mode : public application_mode, private editor_track::observer bool scanned{false}; std::size_t history{0}; std::string profile; - std::vector intruding; std::vector hits; } map; // whole scenery struct diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 7c0ade95..0d444efa 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -36,7 +36,6 @@ namespace { char const *const gauge_file{"editor_structure_gauges.txt"}; -std::size_t const gauge_triangle_limit{200000}; // 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 @@ -290,13 +289,11 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } } -// triangles of the models entering the gauge, and the deepest place of each model, the deepest models first -void scan_models(corridor const &Space, std::vector &Triangles, std::vector &Hits) +// the models entering the gauge with the deepest place of each, the deepest models first +void scan_models(corridor const &Space, std::vector &Hits) { for (auto *instance : simulation::Instances.sequence()) { - if (Triangles.size() >= 3 * gauge_triangle_limit) - break; if (instance == nullptr || false == instance->visible() || instance->Model() == nullptr || instance->Model()->GetSMRoot() == nullptr) continue; if (false == Space.near(instance->location(), instance->radius())) @@ -313,7 +310,6 @@ void scan_models(corridor const &Space, std::vector &Triangles, std: auto const found{Space.intrusion(point)}; if (found.depth <= 0.0) continue; - Triangles.insert(Triangles.end(), {A, B, C}); if (found.depth > hit.depth) { hit.depth = found.depth; @@ -492,7 +488,7 @@ void editor_mode::scan_gauge(TTrack &Track) 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.intruding, m_gauge.line.hits); + scan_models(space, m_gauge.line.hits); if (false == m_gauge.map.scanned) m_gauge.current = -1; } @@ -505,7 +501,7 @@ void editor_mode::scan_gauge_map() auto const choice{gauge_choice()}; auto const lines{scenery_lines(choice)}; corridor const space{lines}; - scan_models(space, m_gauge.map.intruding, m_gauge.map.hits); + scan_models(space, m_gauge.map.hits); 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{"-"}; }}; @@ -523,8 +519,8 @@ 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, bool const Offset, std::vector const &Points) { - gfx::editor_overlay::batch result{Color, Type, Offset}; + 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); @@ -538,13 +534,11 @@ void editor_mode::gauge_publish() 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, false, m_gauge.line.surface), - batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, false, m_gauge.line.edges), - batch({1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true, m_gauge.line.intruding), - batch({1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true, m_gauge.map.intruding), - batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, false, marker), - batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, false, m_gauge.spot.surface), - batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, false, m_gauge.spot.edges)}; + 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) @@ -600,7 +594,7 @@ void editor_mode::render_gauge_window() 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,\nwith the triangles of the models which enter it in red.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II."); + ImGui::SetTooltip("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()) @@ -612,7 +606,7 @@ void editor_mode::render_gauge_window() if (ImGui::Button("Find the violations on the whole map")) scan_gauge_map(); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Checks the models along all tracks of the scenery against the chosen gauge,\nmarks them in red and lists them, the deepest first"); + ImGui::SetTooltip("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first"); if (m_gauge.map.scanned) { ImGui::SameLine(); diff --git a/rendering/editoroverlay.h b/rendering/editoroverlay.h index 6a533165..57442d43 100644 --- a/rendering/editoroverlay.h +++ b/rendering/editoroverlay.h @@ -23,7 +23,6 @@ struct editor_overlay { glm::vec4 color{1.0f}; unsigned int type{0}; // GL primitive - bool offset{false}; // pulled towards the camera, to show over the coplanar surfaces std::vector points; // relative to the origin }; glm::dvec3 origin{0.0}; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 4c769e8f..b146d5d1 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -4531,14 +4531,8 @@ void opengl33_renderer::Render_editor_overlay() { if (batches[i].points.empty() || m_editor_overlay_geometry[i] == null_handle) continue; - if (batches[i].offset) - { - ::glEnable(GL_POLYGON_OFFSET_FILL); - ::glPolygonOffset(-2.f, -2.f); - } model_ubs.param[0] = batches[i].color; draw(m_editor_overlay_geometry[i]); - ::glDisable(GL_POLYGON_OFFSET_FILL); ++m_renderpass.draw_stats.drawcalls; } ::glDepthMask(GL_TRUE); From 818fc60e21be55fc77c2129011f5ea8e69405e31 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:33:11 +0200 Subject: [PATCH 32/34] Structure gauge: no road gauge along the pavements --- application/editormodegauge.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 0d444efa..40699426 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -24,6 +24,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include #include @@ -87,11 +88,27 @@ gauge_line make_line(editor_track::route &Route, gauge::profile const &Profile) return line; } -// 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) +// 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) @@ -105,7 +122,7 @@ 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) && Choice.of(Track->iCategoryFlag) != nullptr; }}; + 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) @@ -119,7 +136,7 @@ std::vector scenery_lines(gauge::choice const &Choice) spans.pop_back(); for (auto const &span : spans) visited.insert(span.track); - lines.push_back(make_line(route, *Choice.of(track->iCategoryFlag))); + lines.push_back(make_line(route, *outline_of(Choice, *track))); } // diverging paths of the switches for (auto *track : simulation::Paths.sequence()) @@ -127,7 +144,7 @@ std::vector scenery_lines(gauge::choice const &Choice) { editor_track::route route; route.spans.push_back({track, 1, true}); - lines.push_back(make_line(route, *Choice.of(track->iCategoryFlag))); + lines.push_back(make_line(route, *outline_of(Choice, *track))); } return lines; } From ac82db81787f7c627c8b49301cc3542b0f2edb15 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:35:00 +0200 Subject: [PATCH 33/34] Structure gauge: map scan in slices over the frames with progress and cancel, skip the triangles far from the lines --- application/editormode.h | 3 + application/editormodegauge.cpp | 177 +++++++++++++++++++++++--------- 2 files changed, 133 insertions(+), 47 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index dd4e4872..770f6a7a 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -347,6 +347,7 @@ class editor_mode : public application_mode, private editor_track::observer } 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 @@ -355,6 +356,8 @@ class editor_mode : public application_mode, private editor_track::observer 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); diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index 40699426..f10d42a4 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -10,6 +10,7 @@ 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" @@ -42,6 +43,8 @@ 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}; @@ -171,7 +174,21 @@ public: return {-Sample.direction.y, Sample.direction.x}; } - // whether the sphere may reach the gauge + // 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 near(glm::dvec3 const &Center, double const Radius) const { auto const center{plan_of(Center)}; @@ -306,42 +323,55 @@ template void visit_triangles(TSubModel *Submodel, glm::dmat4 } } -// the models entering the gauge with the deepest place of each, the deepest models first +// 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.near(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 || false == instance->visible() || instance->Model() == nullptr || instance->Model()->GetSMRoot() == nullptr) - continue; - if (false == Space.near(instance->location(), instance->radius())) - continue; - 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 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)); - } - std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); + 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 @@ -431,6 +461,17 @@ bool edit_points(std::vector &Points) } // 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()) @@ -462,6 +503,8 @@ std::vector const &editor_mode::gauge_hits() const void editor_mode::update_gauge() { + if (m_gauge.scan) + step_gauge_map(); if (false == m_gauge.open) { if (false == EditorOverlay.batches.empty()) @@ -513,21 +556,51 @@ void editor_mode::scan_gauge(TTrack &Track) void editor_mode::scan_gauge_map() { gauge_load(); - auto const started{std::chrono::steady_clock::now()}; + 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 = {}; - auto const choice{gauge_choice()}; - auto const lines{scenery_lines(choice)}; - corridor const space{lines}; - scan_models(space, m_gauge.map.hits); 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(choice.railway) + ", " + name(choice.road); + m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); m_gauge.current = -1; m_gauge.published = false; - WriteLog("Structure gauge (" + m_gauge.map.profile + "): " + std::to_string(m_gauge.map.hits.size()) + " models enter it along " + std::to_string(lines.size()) + " lines, scanned in " + - std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - started).count()) + " ms"); - if (false == m_gauge.map.hits.empty()) + 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); } @@ -620,11 +693,21 @@ void editor_mode::render_gauge_window() if (ImGui::IsItemHovered()) ImGui::SetTooltip("Clearance over the roads, from 0.5 m beyond the edges of the carriageway"); - if (ImGui::Button("Find the violations on the whole map")) + 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(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first"); - if (m_gauge.map.scanned) + 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")) From 9bd8226c51439cb6267afea4af86ca9817105887 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 02:49:05 +0200 Subject: [PATCH 34/34] Fix the GCC 13 and MSVC builds: no brace-initialised reference from reference_wrapper, no near/far names --- application/editormodegauge.cpp | 4 ++-- application/editormodeinfra.cpp | 2 +- application/editormodetrack.cpp | 6 +++--- editor/editorTrack.cpp | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp index f10d42a4..64ff2e61 100644 --- a/application/editormodegauge.cpp +++ b/application/editormodegauge.cpp @@ -189,7 +189,7 @@ public: } // whether the sphere may reach the gauge, roughly - bool near(glm::dvec3 const &Center, double const Radius) const + bool nearby(glm::dvec3 const &Center, double const Radius) const { auto const center{plan_of(Center)}; auto const reach{Radius + m_margin + region}; @@ -328,7 +328,7 @@ void scan_model(corridor const &Space, TAnimModel &Instance, std::vectorGetSMRoot() == nullptr) return; - if (false == Space.near(Instance.location(), Instance.radius())) + 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 diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 67cf2333..ba3213ff 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -359,7 +359,7 @@ bool nearest_beside(Areas_ const &Areas, glm::dvec3 const &Point, double const C auto best{-1.0}; for (auto const &entry : Areas) { - track_area const &area{entry}; + track_area const &area = entry; if (plan_distance(Point, area.centre) > area.radius + Corridor) continue; infra::station station; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4e3b9a94..b3b2c8e2 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3330,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 = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; - m_route.from = far(state.fit_before, Line.start); - m_route.to = far(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; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index d43d3043..c370bf2f 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1201,22 +1201,22 @@ editor_track::straight_run editor_track::straight_beyond(chain const &Chain, boo outward = false; else break; - auto const &far{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + 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(far - joint))}; + 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(far) - origin}; + 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 = far; + joint = opposite; current = outward ? current->trNext : current->trPrev; } return run;