From 93a45fb6328b2bbbb664b3469f3557f981e58d31 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 19 Oct 2026 18:20:41 +0200 Subject: [PATCH] fix(editor): a slip goes to the crossing under the cursor or nowhere, shown before the click A slip went to the crossing of the selected straight with another straight within 40 m of the click. When the track under the cursor wasn't taken as a straight (a slight curve, a switch), the slip silently went to another crossing up to 40 m away. A double slip within 25 m of the click was replaced, even when the click was on a free crossing. - The crossing nearest the cursor, within 12 m, is found among all the rail tracks. A slip goes only there; when that track is a curve or a switch, the tool says so. - A double slip is replaced only when it is nearer the cursor than any crossing. - The cursor is taken at the height of the straight under it, so on a grade the 3D view doesn't put the click metres away from the track. - Before the click, the view marks the crossing the slip goes to, with its two straights, or the reason it can't go there. --- application/editormode.h | 20 +++ application/editormodeswitchplace.cpp | 179 ++++++++++++++++++-------- application/editormodetrack.cpp | 42 ++++++ application/editormodetrackshared.h | 1 + lang/editor_pl.po | 15 +++ 5 files changed, 204 insertions(+), 53 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index b288e394..02bd2afa 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1432,6 +1432,26 @@ class editor_mode : public application_mode, private editor_track::observer bool stretchable(editor_track::snap_target const &Target) const; std::vector link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector> &States, std::vector &Created); bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style); + // where a slip or a crossing goes for the cursor at Point on the straight Line: the crossing of Line with the rail track + // nearest the cursor, or the double slip there to be replaced + struct slip_plan + { + std::string problem; + std::string note; + TTrack *replaces{nullptr}; + TTrack *other{nullptr}; + glm::dvec2 crossing{0.0}; + glm::dvec2 first{0.0, 1.0}; + glm::dvec2 direction{0.0, 1.0}; + double angle{0.0}; + double alongfirst{0.0}; + double half{0.0}; + editor_track::slip_curve curve; + std::array points{}; + }; + bool plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const; + // the cursor where it points at the straight, at the height of the straight there + glm::dvec3 cursor_on_straight(editor_track::straight const &Line) const; bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape); bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape); std::vector 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, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type = "Rkpd"); diff --git a/application/editormodeswitchplace.cpp b/application/editormodeswitchplace.cpp index 217eadc5..2275c361 100644 --- a/application/editormodeswitchplace.cpp +++ b/application/editormodeswitchplace.cpp @@ -124,18 +124,19 @@ bool editor_mode::start_switch_placement() return true; } auto const &line{current_straight()}; - auto const along{std::clamp(line.along(ground), 0.0, line.length)}; + auto const cursor{cursor_on_straight(line)}; + auto const along{std::clamp(line.along(cursor), 0.0, line.length)}; auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along}; - if (plan_distance(point, ground) > kSwitchReach) + if (plan_distance(point, cursor) > kSwitchReach) return false; if (tool.templates[tool.armed].double_slip) { - insert_double_slip(line, ground, tool.templates[tool.armed]); + insert_double_slip(line, cursor, tool.templates[tool.armed]); return true; } tool.along = along; tool.point = point; - tool.mouse = ground; + tool.mouse = cursor; tool.line = line; tool.placing = true; return true; @@ -817,61 +818,99 @@ std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, g return parts; } -bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) +glm::dvec3 editor_mode::cursor_on_straight(editor_track::straight const &Line) const { - auto &state{m_straights}; - state.status.clear(); - { - 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 (Shape.type == "Rkpd" && 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}; + // the height of the straight under the cursor, worked out anew till it settles: on a grade the plane of the start of the + // straight would put the cursor metres away from where it points at the track + auto cursor{cursor_level(Line.start.y)}; + for (int i = 0; i < 4; ++i) + cursor = cursor_level(Line.start.y + Line.grade * std::clamp(Line.along(cursor), 0.0, Line.length)); + return cursor; +} + +bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const +{ + Plan = slip_plan{}; + // the crossing of the straight with the rail track nearest the cursor, whatever that track is: the slip goes there or + // nowhere, never to another crossing around + TTrack *nearest{nullptr}; glm::dvec2 crossing{0.0}; - double best{40.0}; - auto const sections{simulation::Region->sections(Point, 60.f)}; - for (auto *section : sections) - { + double best{kSlipReach}; + auto const category{Line.tracks.empty() ? 1 : Line.tracks.front()->iCategoryFlag & 15}; + for (auto *section : simulation::Region->sections(Point, 300.f)) for (auto const &cell : section->m_cells) - { for (auto *track : cell.m_paths) { - 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)) + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != category || std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end()) continue; - auto const &path{track->m_paths.front()}; - 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_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_of(Line.start) + Line.direction * along}; - auto const distance{glm::distance(position, plan_of(Point))}; - if (distance < best) + for (auto const &path : track->m_paths) { - best = distance; - other = track; - crossing = position; + bezier const curve{path}; + auto const straight{path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}}; + int const pieces{straight ? 1 : 16}; + auto previous{plan_of(path.points[segment_data::point::start])}; + for (int i = 1; i <= pieces; ++i) + { + auto const next{straight ? plan_of(path.points[segment_data::point::end]) : plan_of(curve.point(static_cast(i) / pieces))}; + auto const segment{next - previous}; + auto const determinant{cross(Line.direction, segment)}; + if (std::abs(determinant) > 1e-9) + { + auto const offset{previous - plan_of(Line.start)}; + auto const along{cross(offset, segment) / determinant}; + auto const fraction{cross(offset, Line.direction) / determinant}; + auto const position{plan_of(Line.start) + Line.direction * along}; + if (fraction >= -0.01 && fraction <= 1.01 && along >= 0.0 && along <= Line.length && glm::distance(position, plan_of(Point)) < best) + { + best = glm::distance(position, plan_of(Point)); + nearest = track; + crossing = position; + } + } + previous = next; + } } } + // a double slip nearer the cursor than any crossing is replaced by the one chosen + if (Shape.type == "Rkpd") + { + TTrack *slip{nullptr}; + double closest{25.0}; + for (auto *section : simulation::Region->sections(Point, 40.f)) + 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_of(track->location()), plan_of(Point)) < closest) + { + closest = glm::distance(plan_of(track->location()), plan_of(Point)); + slip = track; + } + if (slip != nullptr && (nearest == nullptr || closest < best)) + { + Plan.replaces = slip; + return true; } } - if (other == nullptr) + if (nearest == nullptr) { - state.status = STR_C("No straight crossing the selected one near the click"); + Plan.problem = STR_C("No track crosses the selected straight under the cursor"); return false; } - auto const second{editor_track::find_straight(*other, m_straights.tolerance)}; - if (second.tracks.empty()) + if (nearest->eType != tt_Normal) + { + Plan.problem = STR_C("The track crossing here is a switch: a slip goes only where two straights cross"); return false; + } + if (false == editor_track::is_straight(*nearest, m_straights.tolerance)) + { + Plan.problem = STR_C("The track crossing here isn't straight: a slip goes only where two straights cross"); + return false; + } + auto const second{editor_track::find_straight(*nearest, m_straights.tolerance)}; + if (second.tracks.empty()) + { + Plan.problem = STR_C("The track crossing here isn't straight: a slip goes only where two straights cross"); + return false; + } auto const first{Line.direction}; auto direction{second.direction}; if (glm::dot(first, direction) < 0.0) @@ -880,7 +919,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv bool const plain{Shape.type == "Skrz"}; if (angle < glm::radians(0.5) || angle > glm::radians(plain ? 89.0 : 30.0)) { - state.status = STR_C("The straights cross at an angle unsuitable for a double slip"); + Plan.problem = STR_C("The straights cross at an angle unsuitable for a double slip"); return false; } auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; @@ -896,7 +935,7 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv half = available; if (half < 2.0) { - state.status = STR_C("Not enough straight track around the crossing"); + Plan.problem = STR_C("Not enough straight track around the crossing"); return false; } } @@ -906,20 +945,54 @@ bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dv half = available; if (curve.radius < 50.0) { - state.status = STR_C("Not enough straight track around the crossing"); + Plan.problem = STR_C("Not enough straight track around the crossing"); return false; } - state.status = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast(curve.radius), static_cast(available)); + Plan.note = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast(curve.radius), static_cast(available)); } - auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; }; auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0}; auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; }; - auto const pointA{at(crossing - first * half, heightfirst(alongfirst - half))}; - auto const pointB{at(crossing + first * half, heightfirst(alongfirst + half))}; - auto const pointC{at(crossing - direction * half, heightsecond(alongsecond - sign * half))}; - auto const pointD{at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; + Plan.other = nearest; + Plan.crossing = crossing; + Plan.first = first; + Plan.direction = direction; + Plan.angle = angle; + Plan.alongfirst = alongfirst; + Plan.half = half; + Plan.curve = curve; + Plan.points = {at(crossing - first * half, heightfirst(alongfirst - half)), at(crossing + first * half, heightfirst(alongfirst + half)), at(crossing - direction * half, heightsecond(alongsecond - sign * half)), + at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; + return true; +} + +bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) +{ + auto &state{m_straights}; + state.status.clear(); + slip_plan plan; + if (false == plan_double_slip(Line, Point, Shape, plan)) + { + state.status = plan.problem; + return false; + } + if (plan.replaces != nullptr) + return replace_double_slip(*plan.replaces, Shape); + state.status = plan.note; + auto *other{plan.other}; + auto const crossing{plan.crossing}; + auto const first{plan.first}; + auto const direction{plan.direction}; + auto const angle{plan.angle}; + auto const alongfirst{plan.alongfirst}; + auto const half{plan.half}; + auto const curve{plan.curve}; + auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; + auto const &pointA{plan.points[0]}; + auto const &pointB{plan.points[1]}; + auto const &pointC{plan.points[2]}; + auto const &pointD{plan.points[3]}; std::vector> states; std::vector created; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 3e9da2f8..1d3f7607 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -2107,6 +2107,48 @@ void editor_mode::draw_build_overlay() const drawlist->AddText(ImVec2(screen.x + 18.0f, screen.y + 12.0f), valid ? (plan.warning.empty() ? overlay_color::highlight : overlay_color::warning) : overlay_color::invalid, text.c_str()); } } + else if (auto const *selected{selected_track()}; false == m_switch.placing && m_track_tab == track_tab::turnout && m_switch.connection < 0 && m_switch.armed >= 0 && m_switch.armed < static_cast(m_switch.templates.size()) && + m_switch.templates[m_switch.armed].double_slip && selected != nullptr && selected == m_straights.current_for && false == m_straights.current.tracks.empty() && + false == ImGui::GetIO().WantCaptureMouse) + { + // where the slip goes for a click there, before the click + auto const &line{m_straights.current}; + auto const cursor{cursor_on_straight(line)}; + auto const along{std::clamp(line.along(cursor), 0.0, line.length)}; + if (plan_distance(line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along, cursor) <= kSwitchReach) + { + slip_plan plan; + auto const valid{plan_double_slip(line, cursor, m_switch.templates[m_switch.armed], plan)}; + std::string text{plan.problem}; + glm::dvec3 marker{cursor}; + ImU32 colour{overlay_color::invalid}; + if (valid && plan.replaces != nullptr) + { + marker = plan.replaces->location(); + colour = overlay_color::highlight; + text = STR_C("Click: this double slip is replaced by the one chosen"); + } + else if (valid) + { + for (int i = 0; i < 4; i += 2) + { + segment_data path; + path.points[segment_data::point::start] = plan.points[i]; + path.points[segment_data::point::end] = plan.points[i + 1]; + drawpath(path, overlay_color::marked); + } + marker = {plan.crossing.x, line.start.y + line.grade * plan.alongfirst, plan.crossing.y}; + colour = overlay_color::highlight; + text = plan.note.empty() ? std::string{STR_C("Click: it goes here")} : plan.note; + } + ImVec2 screen; + if (projection.project(marker, screen)) + { + drawlist->AddCircle(screen, 12.0f, colour, 20, 2.5f); + drawlist->AddText({screen.x + 16.0f, screen.y - 8.0f}, colour, text.c_str()); + } + } + } else if (m_switch.placing) { for (auto const &piece : switch_preview()) diff --git a/application/editormodetrackshared.h b/application/editormodetrackshared.h index c2a45c74..c58d605a 100644 --- a/application/editormodetrackshared.h +++ b/application/editormodetrackshared.h @@ -103,6 +103,7 @@ float const kHandleRadius{10.0f}; double const kLaySnapRadius{5.0}; float const kSnapPixels{16.0f}; double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch +double const kSlipReach{12.0}; // m; a slip goes to the crossing nearest the cursor, if it's this close double const kLineReach{2000.0}; // how far each way the Curve tab follows the line double const kDigitizeFit{0.5}; double const kDrawnReach{100000.0}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 1cd48edf..00afe098 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9919,3 +9919,18 @@ msgstr "Tor leży wyżej lub niżej, niż według NMT jest grunt, o więcej niż msgid "Against the terrain model" msgstr "Także względem NMT" + +msgid "No track crosses the selected straight under the cursor" +msgstr "Pod kursorem żaden tor nie przecina zaznaczonej prostej" + +msgid "The track crossing here is a switch: a slip goes only where two straights cross" +msgstr "Tor, który tu przecina, jest rozjazdem: rozjazd krzyżowy stawia się tylko na przecięciu dwóch prostych" + +msgid "The track crossing here isn't straight: a slip goes only where two straights cross" +msgstr "Tor, który tu przecina, nie jest prosty: rozjazd krzyżowy stawia się tylko na przecięciu dwóch prostych" + +msgid "Click: this double slip is replaced by the one chosen" +msgstr "Klik: ten rozjazd krzyżowy zostanie zastąpiony wybranym" + +msgid "Click: it goes here" +msgstr "Klik: stanie tutaj"