17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-29 13:57:28 +01: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};