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); +}