From 34bd7c853ef5cddc9319927ad9fd3a97f346710f Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 25 Oct 2026 17:26:16 +0100 Subject: [PATCH] fix(editor): no path of a few millimetres laid by the route design, and no camera at no finite place: an arc of a compound curve squeezed to almost nothing in the plan (a share of 0.00002) made pieces of a few millimetres, which the simulation takes for paths of no length (Bad track: zero length spline, it measures a path in steps of 5 cm), laid as tracks with kinks at their joints. alignment::pieces gives such a stretch to the piece next to it, the one between the same joints if there's one, so no piece is shorter than 10 cm, and a piece of no finite length isn't split for ever. A zoom with the wheel aimed at a point which isn't a number doesn't move the camera, and a camera gone to no finite place goes back to where it last was: right after such tracks were laid the graphics driver hung (TDR) and the whole machine froze. A unit test cuts 4000 compound curves of random arcs, transitions and shares into pieces, as the plan lays them over the paths there and all over again, and finds none under 5 cm --- application/editormode.cpp | 21 +++++++++++++++ application/editormode.h | 4 +++ editor/editorAlignment.cpp | 34 ++++++++++++++++++++++- tests/unit/test_alignment.cpp | 51 +++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 49364d4b..51b8e082 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -6006,6 +6006,9 @@ void editor_mode::on_scroll(double const Xoffset, double const Yoffset) // no nearer than 2 m to the point, not through it if (notches > 0.0) step = std::max(0.0, std::min(step, distance - 2.0)); + // a point under the cursor which isn't a number, as one on a broken path, moves the camera nowhere + if (false == std::isfinite(step) || glm::any(glm::isnan(direction)) || glm::any(glm::isinf(direction))) + return; Camera.Pos += direction * step; Global.pCamera = Camera; return; @@ -6062,6 +6065,24 @@ void editor_mode::update_camera(double const Deltatime) // reset window state (will be set again if UI requires it) Global.CabWindowOpen = false; + // a camera at no finite place, as a zoom or a framing aimed at a broken path could put it, draws nothing sane and can + // stall the graphics card: it goes back to where it last was + auto const number = [](auto const &Vector) { return false == glm::any(glm::isnan(Vector)) && false == glm::any(glm::isinf(Vector)); }; + if (number(Camera.Pos) && number(Camera.Angle)) + { + m_camera_finite_pos = Camera.Pos; + m_camera_finite_angle = Camera.Angle; + m_camera_finite = true; + } + else if (m_camera_finite) + { + WriteLog("Editor: the camera went to no finite place, back where it was", logtype::generic); + Camera.Pos = m_camera_finite_pos; + Camera.Angle = m_camera_finite_angle; + Camera.Velocity = glm::dvec3(0.0); + m_focus_active = false; + } + // publish camera back to global copy Global.pCamera = Camera; } diff --git a/application/editormode.h b/application/editormode.h index b78582c4..6cfa504f 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -214,6 +214,10 @@ class editor_mode : public application_mode, private editor_track::observer, pri glm::vec3 m_focus_target_angle{0.0f}; // camera pitch/yaw/roll facing the focused object double m_focus_time{0.0}; double m_focus_duration{0.6}; + // the last place and angles of the camera which were numbers: a camera gone to no finite place goes back there + glm::dvec3 m_camera_finite_pos{0.0, 0.0, 0.0}; + glm::vec3 m_camera_finite_angle{0.0f}; + bool m_camera_finite{false}; double fTime50Hz{0.0}; // bufor czasu dla komunikacji z PoKeys scene::basic_editor m_editor; diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 56cebbcc..232bc451 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -2822,6 +2822,37 @@ std::vector pieces(result const &Result, design const &Design, std auto const interval = [&](span const &Span) { return static_cast(std::upper_bound(Breaks.begin(), Breaks.end(), (chainage_from(Span) + chainage_to(Span)) * 0.5) - Breaks.begin()); }; + // no path of a few millimetres, as an arc of a compound curve squeezed to almost nothing or a joint by the end of an element + // leaves: the simulation takes a path under 5 cm for one of no length. Such a stretch goes to the path next to it, the one + // between the same joints if there's one + double constexpr shortest_piece{0.1}; + for (std::size_t i = 0; i < spans.size() && spans.size() > 1;) + { + if (extent(spans[i]) >= shortest_piece) + { + ++i; + continue; + } + auto const own{interval(spans[i])}; + auto const backward{i > 0 && (i + 1 == spans.size() || interval(spans[i - 1]) == own || interval(spans[i + 1]) != own)}; + if (backward) + { + auto &previous{spans[i - 1]}; + previous.start = chainage_from(previous); + previous.end = chainage_to(spans[i]); + previous.generic = true; + } + else + { + auto &next{spans[i + 1]}; + next.end = chainage_to(next); + next.start = chainage_from(spans[i]); + next.generic = true; + } + spans.erase(spans.begin() + static_cast(i)); + if (i > 0) + --i; + } for (std::size_t k = 0; k < Counts.size(); ++k) { while (true) @@ -2846,7 +2877,8 @@ std::vector pieces(result const &Result, design const &Design, std { auto const from{chainage_from(spans[i])}; auto const length{chainage_to(spans[i]) - from}; - if (length <= geometry::longest_path) + // a length not finite would be split for ever + if (length <= geometry::longest_path || false == std::isfinite(length)) { ++i; continue; diff --git a/tests/unit/test_alignment.cpp b/tests/unit/test_alignment.cpp index 9f1b33a7..e9d37c7e 100644 --- a/tests/unit/test_alignment.cpp +++ b/tests/unit/test_alignment.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace { @@ -747,3 +748,53 @@ TEST_CASE("alignment: a curve laid into a curved free end ends at it with its cu CHECK(glm::distance(glm::dvec2{last.position.x, last.position.z}, end.position) < 1e-3); CHECK(std::none_of(line.warnings.begin(), line.warnings.end(), [](std::string const &Warning) { return Warning.find("adjoining track at the end") != std::string::npos; })); } + +TEST_CASE("alignment: the pieces of a compound curve are never of no length, whatever its arcs and transitions") { + std::mt19937 random{20261025}; + auto const uniform = [&](double const Low, double const High) { return std::uniform_real_distribution(Low, High)(random); }; + auto const pick = [&](int const Count) { return std::uniform_int_distribution(0, Count - 1)(random); }; + int bad{0}, valid{0}; + for (int run = 0; run < 4000 && bad < 5; ++run) { + auto design{straights({600.0 + uniform(0.0, 800.0), 600.0 + uniform(0.0, 800.0)}, {uniform(20.0, 110.0) * pi / 180.0}, 100.0)}; + design.transition_pieces = 1 + pick(2); + design.arc_piece_angle = pick(2) == 0 ? 10.0 : 2.0; + auto &vertex{design.vertices.front()}; + vertex.radius = uniform(300.0, 1500.0); + vertex.transition_in = pick(4) == 0 ? 0.0 : uniform(0.0, 200.0); + vertex.transition_out = pick(4) == 0 ? 0.0 : uniform(0.0, 200.0); + vertex.cant = pick(3) == 0 ? 0.0 : uniform(0.0, 150.0); + vertex.compound = true; + vertex.share = pick(5) == 0 ? 0.0 : uniform(0.0, 1.5); + for (int i = 0, count = 1 + pick(2); i < count; ++i) + vertex.arcs.push_back({uniform(300.0, 1500.0), pick(3) == 0 ? 0.0 : uniform(0.0, 120.0), pick(5) == 0 ? 0.0 : uniform(0.0, 0.5)}); + auto const line{alignment::compute(design)}; + if (false == line.valid) + continue; + ++valid; + // as the plan lays them over the paths there, each kept between the joints it had, or all over again + std::vector joints; + auto const count{static_cast(10 + pick(80))}; + for (std::size_t i = 1; i < count; ++i) { + auto const at{alignment::evaluate(line, line.length * i / count + (pick(2) == 0 ? 0.0 : uniform(-3.0, 3.0)))}; + joints.emplace_back(at.position.x, at.position.z); + } + std::vector intervals; + for (auto const &paths : {alignment::pieces_at(line, design, count, joints, intervals), alignment::pieces(line, design, count)}) + for (std::size_t i = 0; i < paths.size(); ++i) { + auto const &path{paths[i]}; + auto const length{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + auto const finite{std::isfinite(length) && std::isfinite(path.points[segment_data::point::control1].x) && std::isfinite(path.points[segment_data::point::control2].x)}; + // the simulation takes a path under 5 cm for one of no length + if (finite && length >= 0.05) + continue; + ++bad; + std::string arcs; + for (auto const &arc : vertex.arcs) + arcs += " [R " + std::to_string(arc.radius) + " L " + std::to_string(arc.transition) + " share " + std::to_string(arc.share) + "]"; + MESSAGE("piece " << i << " of " << paths.size() << ": " << length << " m; R " << vertex.radius << " L " << vertex.transition_in << " / " << vertex.transition_out << " cant " << vertex.cant << " share " << vertex.share << arcs << ", count " << count << ", pieces " << design.transition_pieces << " angle " << design.arc_piece_angle); + break; + } + } + MESSAGE(valid << " valid designs"); + CHECK(bad == 0); +}