From b19f3de0d486e89d959d2ac06f14e995515cb38c Mon Sep 17 00:00:00 2001 From: maj00r Date: Fri, 2 Oct 2026 22:42:09 +0200 Subject: [PATCH] feat(editor): recognize collinear straights, measure --- application/editormode.cpp | 2 + application/editormode.h | 19 ++++ application/editormodetrack.cpp | 165 +++++++++++++++++++++++++++++++- editor/editorTrack.cpp | 115 ++++++++++++++++++++++ editor/editorTrack.hpp | 19 ++++ 5 files changed, 319 insertions(+), 1 deletion(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 41c8e925..063f46cb 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1013,6 +1013,8 @@ bool editor_mode::update() draw_track_overlay(); if (route_active()) draw_route_overlay(); + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights) + draw_straights_overlay(); // --- 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 a5bc4e15..b0885718 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -319,6 +319,25 @@ class editor_mode : public application_mode glm::dvec3 route_vertex_position(int const Vertex) const; bool route_active() const; route_design m_route; + + struct straights_state + { + editor_track::straight_tolerance tolerance; + double minimum_length{50.0}; + std::vector found; + int listed{-1}; + TTrack *current_for{nullptr}; + editor_track::straight current; + editor_track::straight a; + editor_track::straight b; + char filter[32]{}; + }; + void render_straights_ui(); + void draw_straights_overlay() const; + editor_track::straight const ¤t_straight(); + straights_state m_straights; + enum class track_tab { straights, route, path }; + track_tab m_track_tab{track_tab::straights}; bool m_route_tab{true}; bool m_route_gizmo_using{false}; glm::mat4 m_route_gizmo{1.0f}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 51882ae8..4799ce62 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -423,14 +423,23 @@ void editor_mode::render_track_ui() { if (ImGui::BeginTabBar("##trackediting")) { + if (ImGui::BeginTabItem("Straights")) + { + m_track_tab = track_tab::straights; + m_route_tab = true; + render_straights_ui(); + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem("Route design")) { + m_track_tab = track_tab::route; m_route_tab = true; render_route_ui(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Single path")) { + m_track_tab = track_tab::path; m_route_tab = false; render_path_ui(); ImGui::EndTabItem(); @@ -632,7 +641,7 @@ void editor_mode::render_path_ui() bool editor_mode::route_active() const { - return ui()->mode() == nodebank_panel::TRACK && m_route_tab && false == m_route.chain.tracks.empty(); + return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty(); } void editor_mode::route_recommend(alignment::vertex &Vertex) const @@ -1111,3 +1120,157 @@ void editor_mode::render_route_ui() ImGui::TextUnformatted(route.status.c_str()); } } + +editor_track::straight const &editor_mode::current_straight() +{ + auto *track{selected_track()}; + if (track != m_straights.current_for) + { + m_straights.current_for = track; + m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{}; + } + return m_straights.current; +} + +void editor_mode::draw_straights_overlay() const +{ + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + 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 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; + }; + auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) { + if (Line.tracks.empty()) + return; + projection.line(drawlist, Line.start, Line.end, Color, Thickness); + for (auto const &point : {Line.start, Line.end}) + { + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircleFilled(screen, Thickness + 2.0f, Color); + } + }; + for (int i = 0; i < static_cast(m_straights.found.size()); ++i) + { + 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(m_straights.current, IM_COL32(40, 220, 255, 255), 4.0f); + draw(m_straights.a, IM_COL32(60, 230, 90, 255), 5.0f); + draw(m_straights.b, IM_COL32(255, 60, 255, 255), 5.0f); + if (false == m_straights.a.tracks.empty() && false == m_straights.b.tracks.empty()) + { + auto const &a{m_straights.a}; + for (auto const &point : {m_straights.b.start, m_straights.b.end}) + { + auto const along{glm::dot(glm::dvec2{point.x - a.start.x, point.z - a.start.z}, a.direction)}; + auto const foot{a.start + glm::dvec3{a.direction.x, 0.0, a.direction.y} * along + glm::dvec3{0.0, a.grade * along, 0.0}}; + projection.line(drawlist, foot, point, IM_COL32(255, 210, 60, 220), 1.5f); + } + } +} + +void editor_mode::render_straights_ui() +{ + auto &state{m_straights}; + ImGui::TextDisabled("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to"); + + ImGui::PushItemWidth(100.0f); + 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)); + ImGui::InputDouble("Offset tolerance (m)", &state.tolerance.offset, 0.0, 0.0, "%.3f"); + ImGui::InputDouble("Minimum length (m)", &state.minimum_length, 0.0, 0.0, "%.0f"); + ImGui::PopItemWidth(); + if (ImGui::Button("Recognize straights in the scenery")) + { + state.found = editor_track::find_straights(state.minimum_length, state.tolerance); + 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()); + ImGui::SameLine(); + ImGui::PushItemWidth(150.0f); + ImGui::InputTextWithHint("##straightfilter", "path name", state.filter, sizeof(state.filter)); + ImGui::PopItemWidth(); + ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true); + for (int i = 0; i < static_cast(state.found.size()); ++i) + { + auto const &line{state.found[i]}; + if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; })) + continue; + auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)}; + if (ImGui::Selectable(label.c_str(), state.listed == i)) + { + state.listed = i; + m_node = line.tracks[line.tracks.size() / 2]; + ui()->set_node(m_node); + start_focus(m_node); + } + } + ImGui::EndChild(); + } + + ImGui::Separator(); + auto const &line{current_straight()}; + if (line.tracks.empty()) + { + ImGui::TextDisabled(selected_track() != nullptr ? "The selected path isn't straight" : "No path selected"); + } + else + { + ImGui::Text("Straight of the selected path"); + ImGui::Indent(); + ImGui::TextUnformatted(describe(line).c_str()); + ImGui::Text("From (%.3f, %.3f, %.3f)", line.start.x, line.start.y, line.start.z); + ImGui::Text("To (%.3f, %.3f, %.3f)", line.end.x, line.end.y, line.end.z); + ImGui::Text("Paths: %s ... %s", line.tracks.front()->name().c_str(), line.tracks.back()->name().c_str()); + ImGui::Unindent(); + if (ImGui::Button("Set as A")) + state.a = line; + ImGui::SameLine(); + if (ImGui::Button("Set as B")) + state.b = line; + } + + ImGui::Separator(); + auto const &a{state.a}; + auto const &b{state.b}; + ImGui::TextColored(ImVec4(0.25f, 0.9f, 0.35f, 1.0f), "A: %s", a.tracks.empty() ? "-" : describe(a).c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.25f, 1.0f, 1.0f), "B: %s", b.tracks.empty() ? "-" : describe(b).c_str()); + if (a.tracks.empty() || b.tracks.empty()) + return; + + auto const cross{a.direction.x * b.direction.y - a.direction.y * b.direction.x}; + auto const dot{glm::dot(a.direction, b.direction)}; + auto const between{glm::degrees(std::atan2(std::abs(cross), std::abs(dot)))}; + auto const offset = [&](glm::dvec3 const &Point) { + glm::dvec2 const relative{Point.x - a.start.x, Point.z - a.start.z}; + return a.direction.x * relative.y - a.direction.y * relative.x; + }; + auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - a.start.x, Point.z - a.start.z}, a.direction); }; + auto const height = [&](glm::dvec3 const &Point) { return Point.y - (a.start.y + a.grade * along(Point)); }; + auto const startoffset{offset(b.start)}; + auto const endoffset{offset(b.end)}; + auto const from{std::max(0.0, std::min(along(b.start), along(b.end)))}; + auto const to{std::min(a.length, std::max(along(b.start), along(b.end)))}; + + ImGui::Text("Angle between A and B: %.5f deg%s", between, between < 0.001 ? " (parallel)" : ""); + ImGui::Text("Distance of B from A: start %.3f m, end %.3f m, change %.3f m", std::abs(startoffset), std::abs(endoffset), std::abs(endoffset - startoffset)); + ImGui::Text("B lies on the %s of A", (startoffset + endoffset) > 0.0 ? "left" : "right"); + ImGui::Text("Height of B above A: start %.3f m, end %.3f m", height(b.start), height(b.end)); + ImGui::Text("Grade difference: %.2f per mille", (b.grade * (dot < 0.0 ? -1.0 : 1.0) - a.grade) * 1000.0); + ImGui::Text("Overlap along A: %.2f m", std::max(0.0, to - from)); +} diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 94bb7b45..ca17787a 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -21,7 +21,9 @@ http: #include #include #include +#include #include +#include namespace { @@ -771,6 +773,119 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin return true; } +bool editor_track::is_straight(TTrack const &Track, straight_tolerance const &Tolerance) +{ + if (Track.eType != tt_Normal || false == is_supported(Track) || Track.m_paths.empty()) + return false; + auto const &path{Track.m_paths.front()}; + glm::dvec2 const start{path.points[segment_data::point::start].x, path.points[segment_data::point::start].z}; + glm::dvec2 const end{path.points[segment_data::point::end].x, path.points[segment_data::point::end].z}; + auto const length{glm::distance(start, end)}; + if (length < 1e-3) + return false; + auto const direction{(end - start) / length}; + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + { + auto const &control{path.points[index]}; + glm::dvec2 const offset{control.x, control.z}; + if (std::abs(direction.x * offset.y - direction.y * offset.x) > Tolerance.offset) + return false; + } + return true; +} + +editor_track::straight editor_track::find_straight(TTrack &Track, straight_tolerance const &Tolerance) +{ + straight result; + if (false == is_straight(Track, Tolerance)) + return result; + + auto const plan = [](glm::dvec3 const &Point) { return glm::dvec2{Point.x, Point.z}; }; + auto const &first{Track.m_paths.front()}; + auto const origin{first.points[segment_data::point::start]}; + auto const direction{glm::normalize(plan(first.points[segment_data::point::end] - origin))}; + auto const lateral = [&](glm::dvec3 const &Point) { + auto const offset{plan(Point - origin)}; + return std::abs(direction.x * offset.y - direction.y * offset.x); + }; + auto const continues = [&](TTrack const *Other) { + if (Other == nullptr || false == is_straight(*Other, Tolerance)) + return false; + auto const &path{Other->m_paths.front()}; + auto const otherdirection{glm::normalize(plan(path.points[segment_data::point::end] - path.points[segment_data::point::start]))}; + if (std::abs(direction.x * otherdirection.y - direction.y * otherdirection.x) > Tolerance.angle) + return false; + return lateral(path.points[segment_data::point::start]) <= Tolerance.offset && lateral(path.points[segment_data::point::end]) <= Tolerance.offset; + }; + + std::deque run{&Track}; + for (auto const throughend : {true, false}) + { + TTrack *previous{&Track}; + TTrack *current{throughend ? Track.trNext : Track.trPrev}; + while (continues(current) && std::find(run.begin(), run.end(), current) == run.end()) + { + if (throughend) + run.push_back(current); + else + run.push_front(current); + auto *next{current->trPrev == previous ? current->trNext : current->trPrev}; + previous = current; + current = next; + } + } + + double low{0.0}, high{0.0}; + glm::dvec3 lowpoint{origin}, highpoint{origin}; + for (auto *track : run) + { + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &point{track->m_paths.front().points[index]}; + auto const along{glm::dot(plan(point - origin), direction)}; + if (along < low) + { + low = along; + lowpoint = point; + } + if (along > high) + { + high = along; + highpoint = point; + } + } + } + result.tracks.assign(run.begin(), run.end()); + result.start = lowpoint; + result.end = highpoint; + result.direction = direction; + result.length = high - low; + result.grade = result.length > 0.0 ? (highpoint.y - lowpoint.y) / result.length : 0.0; + auto azimuth{glm::degrees(std::atan2(direction.x, direction.y))}; + if (azimuth < 0.0) + azimuth += 360.0; + result.azimuth = azimuth; + return result; +} + +std::vector editor_track::find_straights(double const Minimumlength, straight_tolerance const &Tolerance) +{ + std::vector result; + std::unordered_set visited; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || visited.count(track) > 0 || false == is_straight(*track, Tolerance)) + continue; + auto line{find_straight(*track, Tolerance)}; + for (auto const *member : line.tracks) + visited.insert(member); + if (line.length >= Minimumlength) + result.push_back(std::move(line)); + } + std::sort(result.begin(), result.end(), [](straight const &A, straight const &B) { return A.length > B.length; }); + return result; +} + std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces) { std::vector created; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 267c0c03..1ed25cae 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -95,6 +95,25 @@ class editor_track double radius{0.0}; }; static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error); + + struct straight + { + std::vector tracks; + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double length{0.0}; + double grade{0.0}; + double azimuth{0.0}; + }; + struct straight_tolerance + { + double angle{0.0002}; + double offset{0.02}; + }; + static bool is_straight(TTrack const &Track, straight_tolerance const &Tolerance); + static straight find_straight(TTrack &Track, straight_tolerance const &Tolerance); + static std::vector find_straights(double const Minimumlength, straight_tolerance const &Tolerance); static std::vector relay(chain const &Chain, std::vector const &Pieces); static void retire(TTrack &Track); static void revive(TTrack &Track);