17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-22 14:23:20 +02:00

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.
This commit is contained in:
maj00r
2026-10-19 18:20:41 +02:00
parent 59ee32da58
commit 93a45fb632
5 changed files with 204 additions and 53 deletions

View File

@@ -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<TTrack *> link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created);
bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &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<glm::dvec3, 4> 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<TTrack *> 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");

View File

@@ -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<TTrack *> 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<double>(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<int>(curve.radius), static_cast<int>(available));
Plan.note = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast<int>(curve.radius), static_cast<int>(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<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;

View File

@@ -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<int>(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())

View File

@@ -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};

View File

@@ -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"