From 16c1ac4c7b7e370f0b52c0411a35a028c92f5fe0 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 11:11:21 +0200 Subject: [PATCH 001/150] feat(editor): two free ends joined without loops or forced curves, the shape from a free end on the wheel - the join of two free ends (a drag from one onto the other, or a click of the lay tool on it) tries in turn: a straight when the joints check can't see the kinks (half its angle), one circular arc through both ends (a piece cut out of a curve comes back as it was, its cant kept), a curve with straights where the tangents meet, a reverse curve, and only then a free Bezier curve; a curve turning more than it needs to, sharper than 10 m or longer than three chords is never taken - the transition curves eased out of a curved neighbour only when there's room for them between the ends, never crossing each other into a loop - cant and transition curves recommended for the radius the curve really gets, at the speed of the joined track or of the tool and never above what the radius allows; the circular arc keeps at least max(20 m, 0.4 V) between the transitions, the cant lowered to what the shortened transitions can ramp, transitions under 1 m left out - shapes from a free end: arc then straight, straight then arc, arc through the cursor, straight; Ctrl+wheel steps the radius, Shift+wheel the shape (at a free end: the other joins), the plain wheel still zooms; a click of the lay tool from a free end lays the shape and goes on from its end - self-test commands join, layto and joints; Polish translation --- application/editormode.cpp | 2 + application/editormode.h | 23 +- application/editormodeselftest.cpp | 96 +++++++ application/editormodetrack.cpp | 425 ++++++++++++++++++++++++----- application/editormodetrackux.cpp | 8 +- editor/editorAlignment.cpp | 263 +++++++++++++++++- editor/editorAlignment.hpp | 49 ++++ lang/editor_pl.po | 70 +++++ 8 files changed, 860 insertions(+), 76 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 32f991c5..d89334e4 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -4994,6 +4994,8 @@ void editor_mode::ortho_pan_stop() void editor_mode::on_scroll(double const Xoffset, double const Yoffset) { + if (false == ImGui::GetIO().WantCaptureMouse && track_wheel(Yoffset != 0.0 ? Yoffset : Xoffset)) + return; if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) return; auto const before{Global.EditorOrthoExtent}; diff --git a/application/editormode.h b/application/editormode.h index c7dbd391..6944c986 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1006,7 +1006,28 @@ class editor_mode : public application_mode, private editor_track::observer // adds a straight (Side 0) or a curve to the left (1) or the right (-1) at the free end of the selected track void extend_add(int const Side); void render_extend_ui(); - bool m_extend_freehand{false}; // dragging the ring of a free end draws a curve through the cursor, not a straight + enum class track_layout + { + arc_straight, + straight_arc, + arc, + straight + }; + track_layout m_track_layout{track_layout::arc_straight}; + double m_join_radius{0.0}; + int m_join_variant{0}; + TTrack const *m_join_track{nullptr}; + editor_track::point_ref m_join_point; + mutable alignment::join_result m_join_last; + bool m_shift_wheel{false}; + bool track_wheel(double const Offset); + std::string track_shaping(bool const Joined) const; + void render_layout_combo(); + bool shift_straight() const; + double design_speed(TTrack const *From, TTrack const *To) const; + std::vector layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const; + std::vector join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const; double m_extend_length{25.0}; double m_extend_radius{300.0}; bool extend_snap(editor_track::snap_target &Target) const; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 98d4a1e2..593ade8a 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -88,6 +88,9 @@ http://mozilla.org/MPL/2.0/. // terrainturn the textures of the palette entry turned // terrainwater [size] material and repeat of a body of water; terrainwaterdefault // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given +// join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other +// layto from the free end at the first point towards the second, as a drag in the layout +// joints ... the joints check over the scenery, the issues around the points logged // screenshot // save // log @@ -971,6 +974,99 @@ void editor_mode::selftest_step() else WriteLog("SELFTEST gauge " + name + ": no such track"); } + else if (command == "join" || command == "layto") + { + glm::dvec3 a{0.0}, b{0.0}; + words >> a.x >> a.z >> b.x >> b.z; + auto const from{editor_track::find_free_end(nullptr, 1, a, 3.0, {})}; + extend_tool tool; + if (from.track == nullptr || false == free_end(*from.track, from.point.path, from.point.kind == editor_track::point_kind::end, tool)) + { + WriteLog("SELFTEST " + command + ": no free end at the first point"); + continue; + } + std::vector pieces; + std::string error; + double length{0.0}; + if (command == "join") + { + double radius{0.0}; + int variant{0}, transitions{0}; + words >> radius >> variant >> transitions; + auto const to{editor_track::find_free_end(nullptr, 1, b, 3.0, {from.track})}; + if (to.track == nullptr) + { + WriteLog("SELFTEST join: no free end at the second point"); + continue; + } + m_join_radius = radius; + m_join_variant = variant; + m_lay.transitions = transitions != 0; + pieces = join_pieces(tool.track, from.point, tool.point, tool.direction, to, length, error); + } + else + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + m_extend = tool; + m_extend.active = true; + m_extend.mouse = {b.x, tool.point.y, b.z}; + m_extend.snap = {}; + pieces = extend_pieces(); + m_extend.active = false; + } + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*tool.track, piece)); + editor_track::commit(created); + if (false == created.empty()) + push_track_snapshot({}, created); + char const *const kinds[]{"none", "straight", "arc", "curve", "reverse curve", "free curve"}; + auto const &join{m_join_last}; + WriteLog(format("SELFTEST %s: %s, %zu paths, R %.1f m, cant %.0f mm, transitions %.1f m, V %.0f km/h, variants %d%s", command.c_str(), kinds[static_cast(join.kind)], pieces.size(), join.radius, join.cant, + join.transition, join.speed, join.variants, error.empty() ? "" : (", " + error).c_str())); + auto heading{tool.direction}; + glm::dvec3 previous{tool.point}; + for (auto const *track : created) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const finish{glm::normalize(geometry::plan_of(curve.first(1.0)))}; + WriteLog(format("SELFTEST %s path %s: L %.2f m, R %.1f m, rolls %.2f %.2f, kink %.4f deg, step %.4f m", command.c_str(), track->name().c_str(), curve.plan_length(), static_cast(path.radius), + static_cast(path.rolls[0]), static_cast(path.rolls[1]), glm::degrees(std::abs(geometry::signed_angle(heading, begin))), glm::distance(previous, path.points[segment_data::point::start]))); + heading = finish; + previous = path.points[segment_data::point::end]; + } + if (false == created.empty()) + WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); + } + else if (command == "joints") + { + std::vector points; + for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) + points.push_back(point); + auto const scope{m_joints.scope}; + m_joints.scope = 1; + joints_start(); + while (m_joints.next < m_joints.queue.size()) + joints_step(); + m_joints.scope = scope; + char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "free end"}; + int around{0}; + for (auto const &issue : m_joints.issues) + { + if (issue.type == joint_issue::kind::open) + continue; + if (std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) + continue; + ++around; + WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", + issue.other != nullptr ? issue.other->name().c_str() : "-")); + } + WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); + } else if (command == "deletetrack") { delete_selected_track(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..7c021098 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -34,6 +34,7 @@ http: #include #include #include +#include namespace { @@ -182,6 +183,37 @@ segment_data turned_around(segment_data const &Path) return result; } +double joint_roll(TTrack const &Track, editor_track::point_ref const &Point) +{ + if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) + return 0.0; + auto const &path{Track.m_paths[Point.path]}; + return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; +} + +std::vector straight_piece(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Length) +{ + if (Length < 0.5) + return {}; + segment_data path; + path.points[segment_data::point::start] = Point; + path.points[segment_data::point::end] = {Point.x + Direction.x * Length, Point.y + Grade * Length, Point.z + Direction.y * Length}; + return {path}; +} + +std::vector arc_through(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec2 const &Offset) +{ + auto const along{glm::dot(Offset, Direction)}; + auto const lateral{cross(Direction, Offset)}; + auto const chord{glm::length(Offset)}; + if (along <= 0.0 || chord < 0.5) + return {}; + if (std::abs(lateral) < 0.005 * chord + 0.05) + return straight_piece(Point, Direction, Grade, along); + auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; + return arc_pieces(Point, Direction, Grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1); +} + // curvature of the shape at Chainage, positive turning from x towards z double curvature_at(alignment::result const &Shape, double const Chainage) { @@ -3466,7 +3498,11 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "straight along it, to another free end: a curve joining them"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; else { - hints = {{"LMB", m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; + bool const chained{m_lay.points.size() == 1 && m_lay.start.track != nullptr}; + hints = {{"LMB", chained ? "lay the shape up to here and go on from its end" : m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Ctrl+wheel", "radius"}}; + if (chained || m_lay.mouse_snap.track != nullptr) + hints.push_back({"Shift+wheel", "arc / straight, at a free end: another shape"}); + hints.insert(hints.end(), {{"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}); if (glm::dvec2 heading; lay_heading(heading)) hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); } @@ -3477,10 +3513,8 @@ std::vector editor_mode::track_key_hints(bool const All) auto const step{std::min(m_straights.detour.size(), 3)}; hints = {{"Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}}; } - else if (m_extend.active && m_extend_freehand) - hints = {{"Drag", "curve through the cursor, along the end: straight"}, {"Ctrl", "straight"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_extend.active) - hints = {{"Drag", "straight along the end, to a free end: a curve joining it"}, {"Shift", "straight, joins nothing"}, {"Release", "build"}, {"Esc", "cancel"}}; + hints = {{"Drag", "the shape chosen towards the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}, {"Shift", "straight, joins nothing"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_switch.placing) hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; else if (m_track_tab == track_tab::lay) @@ -3605,7 +3639,7 @@ std::string editor_mode::track_readout() const auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)}; if (snapped) text += "\njoins " + name(target.track); - return text; + return text + "\n" + track_shaping(snapped); } if (m_lay.active) { @@ -3634,6 +3668,10 @@ std::string editor_mode::track_readout() const text += "\njoins " + name(lay.mouse_snap.track); if (false == lay.preview_error.empty()) text += "\n" + lay.preview_error; + if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr)) + text += "\n" + track_shaping(lay.mouse_snap.track != nullptr); + else if (false == lay.points.empty()) + text += "\n" + format(STR_C("R %.0f m Ctrl+wheel: radius"), lay.radius); return text; } auto const &state{m_straights}; @@ -3697,6 +3735,32 @@ std::string editor_mode::track_readout() const return {}; } +std::string editor_mode::track_shaping(bool const Joined) const +{ + if (false == Joined) + { + char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; + auto text{std::string{STR_C(names[static_cast(m_track_layout)])}}; + if (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) + text += format(" R %.0f m", m_lay.radius); + return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: arc / straight"); + } + auto const &join{m_join_last}; + char const *const kinds[]{"", "straight", "arc", "curve", "reverse curve", "free curve"}; + std::string text{STR_C(kinds[static_cast(join.kind)])}; + if (join.radius > 0.0) + text += format(" R %.0f m", join.radius) + (m_join_radius > 0.0 ? std::string{} : std::string{" "} + STR("(auto)")); + if (join.cant > 0.0) + text += format(STR_C(" cant %.0f mm"), join.cant); + if (join.transition > 0.0) + text += format(STR_C(" transitions %.0f m"), join.transition); + if (join.speed > 0.0 && (join.cant > 0.0 || join.transition > 0.0)) + text += format(" V %.0f km/h", join.speed); + if (join.variants > 1) + text += format(" %d/%d", (m_join_variant % join.variants + join.variants) % join.variants + 1, join.variants); + return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: another shape"); +} + void editor_mode::draw_track_hints() { ImGuiIO const &io = ImGui::GetIO(); @@ -3775,13 +3839,24 @@ void editor_mode::update_build_tools() auto const &io{ImGui::GetIO()}; glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; m_straights.detour_mouse = ground; + if (false == io.KeyShift) + m_shift_wheel = false; + auto const joining = [&](editor_track::snap_target const &Target) { + if (Target.track == m_join_track && Target.point == m_join_point) + return; + m_join_track = Target.track; + m_join_point = Target.point; + m_join_radius = 0.0; + m_join_variant = 0; + }; if (m_extend.active) { screen_projection const projection; glm::dvec3 level; m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast(kMaxPlacementDistance) ? level : ground; typed_extend(); - m_extend.snap = m_typed.empty() && false == io.KeyShift && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + m_extend.snap = m_typed.empty() && false == shift_straight() && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + joining(m_extend.snap); } if (m_switch.placing) m_switch.mouse = cursor_level(m_switch.point.y); @@ -3800,7 +3875,7 @@ void editor_mode::update_build_tools() else if (false == lay_projection.on_level(io.MousePos, height, mouse)) mouse = ground; glm::dvec2 heading; - bool const straight{io.KeyShift && lay_heading(heading)}; + bool const straight{shift_straight() && lay_heading(heading)}; if (straight) { auto const &last{lay.points.back()}; @@ -3809,6 +3884,8 @@ void editor_mode::update_build_tools() } lay.mouse = typed_lay(mouse); lay.mouse_snap = straight ? editor_track::snap_target{} : m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + if (false == m_extend.active) + joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); if (lay.points.empty()) return; auto points{lay.points}; @@ -4115,40 +4192,12 @@ std::vector editor_mode::extend_pieces() const editor_track::snap_target target; if (extend_snap(target)) { - alignment::design design; - design.norms = m_route.design.norms; - design.shape = m_route.design.shape; - design.arc_piece_angle = 90.0; - design.start = tool.point; - design.end = target.position; - design.start_direction = tool.direction; - design.end_direction = glm::normalize(plan_of(target.direction)); - design.start_grade = tool.grade; - design.end_grade = grade_of(target.direction); - auto const speed{editor_track::velocity(*tool.track)}; - design.speed = speed > 0.0 ? speed : 100.0; - if (false == alignment::collinear(design)) - { - alignment::vertex vertex; - vertex.radius = 100000.0; - auto const recommended{alignment::recommend(design.speed, 1000.0, design.norms)}; - vertex.transition_in = vertex.transition_out = recommended.transition; - glm::dvec2 intersection; - if (alignment::tangent_intersection(design, intersection)) - { - design.vertices.push_back(vertex); - } - else - { - vertex.offset = plan_distance(design.start, design.end) / 3.0; - design.vertices = {vertex, vertex}; - } - } - // a track which doesn't fit to the free end isn't built towards the cursor instead, it'd miss the end - auto const shape{alignment::compute(design)}; - return shape.valid ? alignment::pieces(shape, design, 0) : result; + double length; + std::string error; + return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); } - if (false == ImGui::GetIO().KeyShift && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) + m_join_last = {}; + if (false == shift_straight() && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) { auto const &diverging{tool.track->m_paths[1]}; bezier const curve{diverging}; @@ -4170,26 +4219,156 @@ std::vector editor_mode::extend_pieces() const return pieces; } } - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + if (shift_straight()) + return straight_piece(tool.point, tool.direction, tool.grade, glm::dot(glm::dvec2{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}, tool.direction)); + auto radius{m_lay.radius}; + if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) + radius = std::abs(values[0]); + return layout_pieces(tool.point, tool.direction, tool.grade, tool.mouse, radius); +} + +std::vector editor_mode::layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const +{ + glm::dvec2 const offset{Mouse.x - Point.x, Mouse.z - Point.z}; + auto const along{glm::dot(offset, Direction)}; + auto const lateral{cross(Direction, offset)}; auto const chord{glm::length(offset)}; if (chord < 0.5) - return result; - auto const &direction{tool.direction}; - auto const along{glm::dot(offset, direction)}; - auto const lateral{direction.x * offset.y - direction.y * offset.x}; - auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; - if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift || std::abs(lateral) < 0.005 * chord + 0.05) + return {}; + if (m_track_layout == track_layout::straight) + return straight_piece(Point, Direction, Grade, along); + if (m_track_layout == track_layout::arc || std::abs(lateral) < 0.005 * chord + 0.05) + return arc_through(Point, Direction, Grade, offset); + int const side{lateral > 0.0 ? 1 : -1}; + glm::dvec2 const normal{-Direction.y * side, Direction.x * side}; + auto const radius{std::max(1.0, Radius)}; + if (radius >= chord * chord / (2.0 * std::abs(lateral))) + return arc_through(Point, Direction, Grade, offset); + if (m_track_layout == track_layout::arc_straight) { - if (along < 0.5) - return result; - segment_data path; - path.points[segment_data::point::start] = tool.point; - path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along}; - result.push_back(path); - return result; + auto const centre{plan_of(Point) + normal * radius}; + auto const towards{plan_of(Mouse) - centre}; + auto const distance{glm::length(towards)}; + auto const spread{std::acos(std::clamp(radius / distance, -1.0, 1.0))}; + auto const radial0{plan_of(Point) - centre}; + double rest{-1.0}; + double angle{0.0}; + for (auto const sign : {1.0, -1.0}) + { + auto const radial{turned(towards / distance, sign * spread) * radius}; + auto turn{side * signed_angle(radial0, radial)}; + if (turn < 0.0) + turn += 2.0 * glm::pi(); + auto const heading{turned(Direction, side * turn)}; + auto const remaining{glm::dot(plan_of(Mouse) - centre - radial, heading)}; + if (remaining > 0.0 && (rest < 0.0 || turn < angle)) + { + rest = remaining; + angle = turn; + } + } + if (rest < 0.0 || angle > glm::pi()) + return {}; + auto pieces{arc_pieces(Point, Direction, Grade, radius, angle, side)}; + auto const tail{straight_piece(pieces.back().points[segment_data::point::end], turned(Direction, side * angle), Grade, rest)}; + pieces.insert(pieces.end(), tail.begin(), tail.end()); + return pieces; } - auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; - return arc_pieces(tool.point, direction, tool.grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1); + auto const across{radius - std::abs(lateral)}; + if (std::abs(lateral) > 2.0 * radius) + return arc_through(Point, Direction, Grade, offset); + auto const lead{along - std::sqrt(std::max(0.0, radius * radius - across * across))}; + if (lead < 0.5) + return arc_through(Point, Direction, Grade, offset); + auto const angle{2.0 * std::atan2(std::abs(lateral), along - lead)}; + if (angle > glm::pi()) + return {}; + auto pieces{straight_piece(Point, Direction, Grade, lead)}; + auto const arc{arc_pieces(pieces.back().points[segment_data::point::end], Direction, Grade, radius, angle, side)}; + pieces.insert(pieces.end(), arc.begin(), arc.end()); + return pieces; +} + +double editor_mode::design_speed(TTrack const *From, TTrack const *To) const +{ + double speed{0.0}; + for (auto const *track : {From, To}) + { + if (track == nullptr) + continue; + auto const velocity{editor_track::velocity(*track)}; + if (velocity > 0.0) + speed = speed > 0.0 ? std::min(speed, velocity) : velocity; + } + if (speed <= 0.0) + speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; + return speed; +} + +std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const +{ + alignment::join_request request; + request.start.position = Start; + request.start.direction = Direction; + request.start.roll = From != nullptr ? -joint_roll(*From, Frompoint) : 0.0; + request.end.position = End.position; + request.end.direction = plan_of(End.direction); + request.end.roll = End.track != nullptr ? joint_roll(*End.track, End.point) : 0.0; + request.radius = m_join_radius; + request.variant = m_join_variant; + request.transitions = m_lay.transitions; + request.speed = design_speed(From, End.track); + request.kink = m_joints.angle * 0.5; + request.shape = m_route.design.shape; + request.norms = m_route.design.norms; + auto result{alignment::join(request)}; + if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) + { + auto const grade{(End.position.y - Start.y) / std::max(1.0, plan_distance(Start, End.position))}; + auto const in{From != nullptr && From->eType == tt_Normal ? ease_out_of(*From, Start, grade, request.speed, request.norms) : easing{}}; + auto const out{End.track != nullptr && End.track->eType == tt_Normal ? ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : easing{}}; + if (false == in.pieces.empty() || false == out.pieces.empty()) + { + auto middle{request}; + if (false == in.pieces.empty()) + { + middle.start.position = in.end; + middle.start.direction = in.direction; + middle.start.roll = in.pieces.back().rolls[1]; + } + if (false == out.pieces.empty()) + { + middle.end.position = out.end; + middle.end.direction = -out.direction; + middle.end.roll = -out.pieces.back().rolls[1]; + } + auto const gap{plan_of(middle.end.position) - plan_of(middle.start.position)}; + if (glm::length(gap) > 5.0 && glm::dot(glm::normalize(middle.start.direction), gap) > 0.0 && glm::dot(glm::normalize(middle.end.direction), gap) > 0.0) + { + auto eased{alignment::join(middle)}; + if (eased.kind != alignment::join_kind::none && eased.kind != alignment::join_kind::bezier) + { + std::vector pieces{in.pieces}; + pieces.insert(pieces.end(), eased.pieces.begin(), eased.pieces.end()); + for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece) + pieces.push_back(turned_around(*piece)); + eased.pieces = std::move(pieces); + eased.length += (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length); + result = std::move(eased); + } + } + } + } + Length = result.length; + Error = result.kind == alignment::join_kind::bezier ? std::string{STR_C("No curve fits between the ends, they are joined with a free curve")} : std::string{}; + m_join_last = result; + return result.pieces; +} + +bool editor_mode::shift_straight() const +{ + return ImGui::GetIO().KeyShift && false == m_shift_wheel; } void editor_mode::finish_extend() @@ -4299,8 +4478,70 @@ void editor_mode::render_extend_ui() ImGui::SameLine(); if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) extend_add(-1); - ImGui::Checkbox(STR_C("Freehand"), &m_extend_freehand); - item_tooltip("Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end"); + ImGui::PushID("extend"); + render_layout_combo(); + ImGui::PopID(); +} + +void editor_mode::render_layout_combo() +{ + char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; + auto current{static_cast(m_track_layout)}; + ImGui::PushItemWidth(200.0f); + if (ImGui::BeginCombo(STR_C("Shape from a free end"), STR_C(names[current]))) + { + for (int i = 0; i < 4; ++i) + if (ImGui::Selectable(STR_C(names[i]), i == current)) + m_track_layout = static_cast(i); + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + item_tooltip("What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius"); +} + +bool editor_mode::track_wheel(double const Offset) +{ + auto const &io{ImGui::GetIO()}; + if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) + return false; + bool const laying{m_lay.active && false == m_lay.points.empty()}; + if (false == m_extend.active && false == laying) + return false; + int const step{Offset > 0.0 ? 1 : -1}; + editor_track::snap_target target; + bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr}; + static double const radii[]{100.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0, 4000.0, 6000.0}; + auto const next_radius = [&](double const Current) { + if (step > 0) + { + for (auto const radius : radii) + if (radius > Current * 1.001) + return radius; + return std::end(radii)[-1]; + } + for (auto radius{std::rbegin(radii)}; radius != std::rend(radii); ++radius) + if (*radius < Current * 0.999) + return *radius; + return radii[0]; + }; + if (io.KeyCtrl) + { + if (false == joined) + m_lay.radius = next_radius(m_lay.radius); + else if (m_join_radius <= 0.0) + m_join_radius = next_radius(m_join_last.radius > 0.0 ? m_join_last.radius : 500.0); + else if (step > 0 && m_join_radius >= std::end(radii)[-1]) + m_join_radius = 0.0; + else + m_join_radius = next_radius(m_join_radius); + return true; + } + m_shift_wheel = true; + if (joined) + m_join_variant += step; + else + m_track_layout = static_cast((static_cast(m_track_layout) + 4 - step) % 4); + return true; } std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const @@ -4309,6 +4550,24 @@ std::vector editor_mode::lay_pieces(std::vector const Length = 0.0; if (Points.size() < 2) return {}; + if (Points.size() == 2 && (End.track != nullptr || tool.start.track != nullptr)) + { + auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + if (glm::length(leaving) < 1e-6) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + if (End.track != nullptr) + return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); + m_join_last = {}; + auto const pieces{layout_pieces(Points[0], glm::normalize(leaving), -grade_of(tool.start.direction), Points[1], tool.radius)}; + for (auto const &piece : pieces) + Length += bezier{piece}.plan_length(); + if (pieces.empty()) + Error = STR_C("The track can't be laid through these points"); + return pieces; + } auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) @@ -4338,9 +4597,15 @@ std::vector editor_mode::lay_pieces(std::vector const result.pieces.clear(); return result; }; - auto const in{eased(tool.start, 0.0, 1.0)}; - auto const out{eased(End, shape.length, -1.0)}; - if (false == in.pieces.empty() || false == out.pieces.empty()) + auto const in{tool.transitions ? eased(tool.start, 0.0, 1.0) : easing{}}; + auto const out{tool.transitions ? eased(End, shape.length, -1.0) : easing{}}; + auto const room = [&]() { + auto const from{in.pieces.empty() ? Points.front() : in.end}; + auto const to{out.pieces.empty() ? Points.back() : out.end}; + auto const gap{plan_of(to) - plan_of(from)}; + return glm::length(gap) > 5.0 && glm::dot(in.pieces.empty() ? design.start_direction : in.direction, gap) > 0.0 && glm::dot(out.pieces.empty() ? design.end_direction : -out.direction, gap) > 0.0; + }; + if ((false == in.pieces.empty() || false == out.pieces.empty()) && room()) { auto points{Points}; auto starting{design.start_direction}; @@ -4380,7 +4645,7 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, design = {}; design.norms = m_route.design.norms; design.shape = m_route.design.shape; - design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0; + design.speed = design_speed(tool.start.track, nullptr); design.start = Points.front(); design.end = Points.back(); design.start_direction = Startdirection; @@ -4388,12 +4653,6 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, alignment::vertex corner; corner.radius = std::max(1.0, tool.radius); - if (tool.transitions) - { - auto const recommended{alignment::recommend(design.speed, corner.radius, design.norms)}; - corner.cant = recommended.cant; - corner.transition_in = corner.transition_out = recommended.transition; - } if (Points.size() == 2) { if (false == alignment::collinear(design)) @@ -4423,7 +4682,7 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); } } - return alignment::compute(design); + return alignment::compute_recommended(design, tool.transitions); } void editor_mode::lay_click() @@ -4447,8 +4706,31 @@ void editor_mode::lay_click() auto const point{placement_on_ground(tool.mouse)}; if (plan_distance(point, tool.points.back()) < 1.0) return; - tool.points.push_back(point); m_typed.clear(); + if (tool.points.size() == 1 && tool.start.track != nullptr) + { + tool.points.push_back(tool.mouse); + lay_finish({}); + if (false == tool.points.empty()) + { + tool.points.pop_back(); + return; + } + auto *last{dynamic_cast(m_node)}; + if (last == nullptr || last->m_paths.empty()) + return; + auto const &path{last->m_paths.front()}; + editor_track::snap_target next; + next.track = last; + next.point = {0, editor_track::point_kind::end}; + next.position = path.points[segment_data::point::end]; + auto const &control{path.points[segment_data::point::control2]}; + next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position); + tool.start = next; + tool.points = {next.position}; + return; + } + tool.points.push_back(point); } void editor_mode::lay_finish(editor_track::snap_target const &End) @@ -4569,8 +4851,9 @@ void editor_mode::render_lay_ui() tool.radius = std::max(10.0, tool.radius); ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); - item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant"); + item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); ImGui::PopItemWidth(); + render_layout_combo(); if (false == ImGui::TreeNode(STR_C("More settings"))) { render_lay_status(); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 9e19d1b2..158172e4 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -731,7 +731,7 @@ char const *editor_mode::typed_meaning() const { auto const &straights{m_straights}; if (m_extend.active) - return m_extend_freehand && false == ImGui::GetIO().KeyCtrl ? "type the radius, a space, the length" : "type the length"; + return m_track_layout == track_layout::straight || shift_straight() ? "type the length" : m_track_layout == track_layout::arc ? "type the radius, a space, the length" : "type the radius"; if (straights.dragging) return straights.handle == 2 ? "type the shift, + to the right" : "type the length"; if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging)) @@ -800,7 +800,9 @@ void editor_mode::typed_extend() return; auto &tool{m_extend}; auto const &direction{tool.direction}; - if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl) + if (m_track_layout != track_layout::straight && m_track_layout != track_layout::arc && false == shift_straight()) + return; + if (m_track_layout == track_layout::straight || shift_straight()) { auto const length{std::max(0.0, values[0])}; tool.mouse = {tool.point.x + direction.x * length, tool.mouse.y, tool.point.z + direction.y * length}; @@ -817,7 +819,7 @@ void editor_mode::typed_extend() auto angle{values.size() > 1 ? std::abs(values[1]) / radius : side * geometry::signed_angle(radial, glm::dvec2{tool.mouse.x, tool.mouse.z} - centre)}; if (angle < 0.0) angle += 2.0 * glm::pi(); - angle = std::clamp(angle, 1e-3, 2.0 * glm::pi() - 0.1); + angle = std::clamp(angle, 1e-3, glm::pi()); auto const end{centre + geometry::turned(radial, side * angle)}; tool.mouse = {end.x, tool.mouse.y, end.y}; } diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index cf894d63..cc32a0ce 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -17,6 +17,7 @@ http: #include #include #include +#include namespace alignment { @@ -378,6 +379,7 @@ result fit_between(design const &Design) double tangent_in{0.0}; double tangent_out{0.0}; double deflection{0.0}; + double cant{0.0}; bool compound{false}; std::vector arcs; std::function tangents; @@ -518,6 +520,44 @@ result fit_between(design const &Design) } r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); } + for (std::size_t k = 0; k < count; ++k) + { + auto &fit{fits[k]}; + auto const &vertex{Design.vertices[k]}; + fit.cant = std::max(0.0, vertex.cant); + if (fit.compound || fit.deflection < 1e-7 || fit.transition_in + fit.transition_out <= 0.0) + continue; + auto const angles = [&](double const Factor) { + return transition_point(Design.shape, fit.transition_in * Factor, fit.radius, 1.0).angle + transition_point(Design.shape, fit.transition_out * Factor, fit.radius, 1.0).angle; + }; + auto const minimum{std::min(fit.radius * fit.deflection, std::max(Design.norms.arc_min, Design.norms.arc_min_factor * Design.speed))}; + if (fit.radius * (fit.deflection - angles(1.0)) < minimum - 1e-6) + { + auto const target{fit.deflection - minimum / fit.radius}; + double low{0.0}, high{1.0}; + for (int i = 0; i < 50; ++i) + { + auto const middle{(low + high) * 0.5}; + (angles(middle) > target ? high : low) = middle; + } + fit.transition_in *= low; + fit.transition_out *= low; + r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to %.1f / %.1f m, the circular arc keeps %.0f m", k + 1, fit.transition_in, fit.transition_out, minimum)); + } + if (fit.transition_in < 1.0) + fit.transition_in = 0.0; + if (fit.transition_out < 1.0) + fit.transition_out = 0.0; + auto const shortest{std::min(fit.transition_in, fit.transition_out)}; + if (fit.cant <= 0.0 || (fit.transition_in >= vertex.transition_in - 1e-6 && fit.transition_out >= vertex.transition_out - 1e-6)) + continue; + auto const speed{std::max(1.0, Design.speed)}; + auto const ramp{std::min(shortest * 1000.0 / (Design.norms.ramp_factor * speed), Design.norms.cant_rate * 3.6 * shortest / speed)}; + if (ramp >= fit.cant) + continue; + fit.cant = ramp < 5.0 ? 0.0 : std::floor(ramp / 5.0) * 5.0; + r.warnings.emplace_back(format("Vertex %zu: cant lowered to %.0f mm, the shortened transition curves can't ramp more", k + 1, fit.cant)); + } std::vector vertexchainage(count, 0.0); glm::dvec2 cursor{start}; @@ -650,7 +690,7 @@ result fit_between(design const &Design) curve.vertex = static_cast(k); curve.radius = radius; curve.turn = side; - curve.cant = std::max(0.0, vertex.cant); + curve.cant = fit.cant; if (lengthin > 0.0) { curve.kind = element_kind::transition_in; @@ -804,6 +844,227 @@ result compute(design const &Design) return r; } +double speed_limit(double const Radius, limits const &Limits) +{ + if (Radius <= 0.0) + return 0.0; + auto const factor{Limits.gauge / (kGravity * 12.96)}; + auto const allowance{Limits.gauge * Limits.unbalanced / kGravity}; + return std::sqrt(Radius * (Limits.cant_max + allowance) / factor); +} + +result compute_recommended(design &Design, bool const Transitions) +{ + for (auto &vertex : Design.vertices) + { + vertex.cant = 0.0; + vertex.transition_in = vertex.transition_out = 0.0; + } + auto r{compute(Design)}; + if (false == Transitions || false == r.valid) + return r; + auto const speed{Design.speed}; + for (int i = 0; i < 3; ++i) + { + bool accepted{false}; + for (auto const share : {1.0, 0.75, 0.5, 0.35, 0.25}) + { + auto attempt{Design}; + auto slowest{speed}; + for (auto const &curve : r.curves) + { + if (curve.vertex < 0 || curve.vertex >= static_cast(attempt.vertices.size())) + continue; + auto const allowed{std::max(1.0, std::min(speed, speed_limit(curve.radius, attempt.norms)))}; + auto const recommended{recommend(allowed, curve.radius, attempt.norms)}; + auto const length{recommended.transition * share}; + auto &vertex{attempt.vertices[curve.vertex]}; + vertex.transition_in = vertex.transition_out = length; + vertex.cant = std::min({recommended.cant, length * 1000.0 / (attempt.norms.ramp_factor * allowed), attempt.norms.cant_rate * 3.6 * length / allowed}); + vertex.cant = vertex.cant < 5.0 ? 0.0 : std::floor(vertex.cant / 5.0) * 5.0; + slowest = std::min(slowest, allowed); + } + attempt.speed = slowest; + auto next{compute(attempt)}; + if (false == next.valid) + continue; + auto const kept{std::all_of(next.curves.begin(), next.curves.end(), [&](curve_report const &Curve) { + auto const &vertex{attempt.vertices[Curve.vertex]}; + return Curve.transition_in >= 0.95 * vertex.transition_in && Curve.transition_out >= 0.95 * vertex.transition_out; + })}; + if (false == kept && share > 0.25) + continue; + Design = attempt; + r = next; + accepted = true; + break; + } + if (false == accepted) + break; + } + return r; +} + +namespace +{ + +double turning(result const &Result) +{ + double total{0.0}; + for (auto const &e : Result.elements) + { + if (e.radius <= 0.0) + continue; + if (e.kind == element_kind::arc) + total += e.length / e.radius; + else if (e.kind == element_kind::transition_in || e.kind == element_kind::transition_out) + total += e.length / (2.0 * e.radius); + } + return total; +} + +void spread_rolls(std::vector &Pieces, double const Start, double const End) +{ + auto const count{static_cast(Pieces.size())}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + Pieces[i].rolls = {static_cast(Start + (End - Start) * i / count), static_cast(Start + (End - Start) * (i + 1) / count)}; +} + +} // namespace + +join_result join(join_request const &Request) +{ + join_result chosen; + auto const &a{Request.start}; + auto const &b{Request.end}; + auto const chord{plan_of(b.position) - plan_of(a.position)}; + auto const distance{glm::length(chord)}; + if (distance < 0.05 || glm::length(a.direction) < 1e-9 || glm::length(b.direction) < 1e-9) + return chosen; + auto const startdirection{glm::normalize(a.direction)}; + auto const enddirection{glm::normalize(b.direction)}; + auto const tolerance{glm::radians(Request.kink)}; + auto const heading{std::abs(geometry::signed_angle(startdirection, enddirection))}; + auto const rise{b.position.y - a.position.y}; + std::vector candidates; + + if (std::abs(geometry::signed_angle(startdirection, chord)) <= tolerance && std::abs(geometry::signed_angle(chord, enddirection)) <= tolerance) + { + join_result straight; + straight.kind = join_kind::straight; + segment_data path; + path.points[segment_data::point::start] = a.position; + path.points[segment_data::point::end] = b.position; + straight.pieces = {path}; + spread_rolls(straight.pieces, a.roll, b.roll); + straight.length = glm::distance(a.position, b.position); + candidates.push_back(straight); + } + + auto const lateral{cross(startdirection, chord)}; + auto const along{glm::dot(startdirection, chord)}; + if (std::abs(lateral) > 1e-6) + { + auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; + int const side{lateral > 0.0 ? 1 : -1}; + auto const radius{distance * distance / (2.0 * std::abs(lateral))}; + if (angle < kPi - 1e-3 && std::abs(geometry::signed_angle(turned(startdirection, side * angle), enddirection)) <= tolerance) + { + join_result arc; + arc.kind = join_kind::arc; + arc.pieces = geometry::arc_pieces(a.position, startdirection, rise / (radius * angle), radius, angle, side); + arc.pieces.back().points[segment_data::point::end] = b.position; + spread_rolls(arc.pieces, a.roll, b.roll); + arc.radius = radius; + arc.length = radius * angle; + candidates.push_back(arc); + } + } + + auto const fitted = [&](join_kind const Kind) { + join_result curve; + design shape; + shape.norms = Request.norms; + shape.shape = Request.shape; + shape.speed = Request.speed; + shape.arc_piece_angle = 90.0; + shape.start = a.position; + shape.end = b.position; + shape.start_direction = startdirection; + shape.end_direction = enddirection; + shape.start_grade = shape.end_grade = rise / distance; + vertex corner; + corner.radius = Request.radius > 0.0 ? Request.radius : 100000.0; + if (Kind == join_kind::curve) + shape.vertices = {corner}; + else + { + corner.offset = distance / 3.0; + shape.vertices = {corner, corner}; + } + auto const r{compute_recommended(shape, Request.transitions)}; + auto const sharp{std::any_of(r.curves.begin(), r.curves.end(), [](curve_report const &Report) { return Report.radius < 10.0; })}; + if (false == r.valid || sharp || turning(r) > heading + kPi + 1e-6 || r.length > 3.0 * distance + 1.0) + return curve; + curve.kind = Kind; + curve.pieces = pieces(r, shape, 0); + if (curve.pieces.empty()) + return curve; + curve.pieces.front().rolls[0] = static_cast(a.roll); + curve.pieces.back().rolls[1] = static_cast(b.roll); + curve.length = r.length; + curve.speed = shape.speed; + curve.warnings = r.warnings; + curve.radius = std::numeric_limits::max(); + for (auto const &report : r.curves) + { + curve.radius = std::min(curve.radius, report.radius); + curve.transition = std::max({curve.transition, report.transition_in, report.transition_out}); + } + for (auto const &e : r.elements) + curve.cant = std::max(curve.cant, e.cant); + if (r.curves.empty()) + curve.radius = 0.0; + return curve; + }; + design probe; + probe.start = a.position; + probe.end = b.position; + probe.start_direction = startdirection; + probe.end_direction = enddirection; + glm::dvec2 intersection; + if (tangent_intersection(probe, intersection)) + { + if (auto curve{fitted(join_kind::curve)}; curve.kind != join_kind::none) + candidates.push_back(curve); + } + else if (auto curve{fitted(join_kind::reverse_curve)}; curve.kind != join_kind::none) + candidates.push_back(curve); + + if (candidates.empty()) + { + join_result fallback; + fallback.kind = join_kind::bezier; + auto const grade{rise / distance}; + auto const handle{distance / 3.0}; + segment_data path; + path.points[segment_data::point::start] = a.position; + path.points[segment_data::point::end] = b.position; + path.points[segment_data::point::control1] = glm::dvec3{startdirection.x, grade, startdirection.y} * handle; + path.points[segment_data::point::control2] = -glm::dvec3{enddirection.x, grade, enddirection.y} * handle; + fallback.pieces = {path}; + spread_rolls(fallback.pieces, a.roll, b.roll); + fallback.length = distance; + candidates.push_back(fallback); + } + auto const count{static_cast(candidates.size())}; + chosen = candidates[((Request.variant % count) + count) % count]; + chosen.variants = count; + if (chosen.speed <= 0.0) + chosen.speed = Request.speed; + return chosen; +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { auto const start{plan_of(Design.start)}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 732bd454..914e7a7a 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -35,6 +35,8 @@ struct limits double vertical_factor{0.4}; double vertical_min{2000.0}; double tangent_min_time{0.0}; + double arc_min{20.0}; + double arc_min_factor{0.4}; }; struct compound_arc @@ -164,6 +166,8 @@ struct result }; result compute(design const &Design); +double speed_limit(double const Radius, limits const &Limits); +result compute_recommended(design &Design, bool const Transitions); bool tangent_intersection(design const &Design, glm::dvec2 &Point); bool collinear(design const &Design); @@ -184,4 +188,49 @@ std::size_t minimum_pieces(result const &Result, design const &Design); std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks = {}, std::vector const &Counts = {}, std::vector *Intervals = nullptr); +struct join_end +{ + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double roll{0.0}; +}; + +enum class join_kind +{ + none, + straight, + arc, + curve, + reverse_curve, + bezier +}; + +struct join_request +{ + join_end start; + join_end end; + double radius{0.0}; + int variant{0}; + bool transitions{false}; + double speed{100.0}; + double kink{0.1}; + transition_shape shape{transition_shape::cubic_parabola}; + limits norms; +}; + +struct join_result +{ + join_kind kind{join_kind::none}; + std::vector pieces; + double length{0.0}; + double radius{0.0}; + double cant{0.0}; + double transition{0.0}; + double speed{0.0}; + int variants{0}; + std::vector warnings; +}; + +join_result join(join_request const &Request); + } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b30109..54e4f947 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5213,3 +5213,73 @@ msgstr "Punkt znikania" msgid "Pedestrian crossing" msgstr "Przejście dla pieszych" + +# scenery editor: joining free ends, the shape from a free end on the wheel +msgid "No curve fits between the ends, they are joined with a free curve" +msgstr "Między końcami nie mieści się żaden łuk, połączono je krzywą swobodną" + +msgid "Arc, then straight" +msgstr "Łuk, potem prosta" + +msgid "Straight, then arc" +msgstr "Prosta, potem łuk" + +msgid "Arc through the cursor" +msgstr "Łuk przez kursor" + +msgid "Shape from a free end" +msgstr "Kształt z wolnego końca" + +msgid "What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius" +msgstr "Co przeciągnięcie lub kliknięcie z wolnego końca kładzie w stronę kursora; Shift+kółko zmienia kształt, Ctrl+kółko promień" + +msgid "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows" +msgstr "Prędkość nowych torów i prędkość projektowa krzywych przejściowych oraz przechyłki; pierwszeństwo ma prędkość toru, do którego się dołącza, i nigdy nie wyższa, niż pozwala promień" + +msgid "R %.0f m Ctrl+wheel: radius" +msgstr "R %.0f m Ctrl+kółko: promień" + +msgid "Ctrl+wheel: radius, Shift+wheel: arc / straight" +msgstr "Ctrl+kółko: promień, Shift+kółko: łuk / prosta" + +msgid "Ctrl+wheel: radius, Shift+wheel: another shape" +msgstr "Ctrl+kółko: promień, Shift+kółko: inny kształt" + +msgid "arc" +msgstr "łuk" + +msgid "curve" +msgstr "łuk z prostymi" + +msgid "reverse curve" +msgstr "łuk odwrotny (S)" + +msgid "free curve" +msgstr "krzywa swobodna" + +msgid "(auto)" +msgstr "(auto)" + +msgid " cant %.0f mm" +msgstr " przechyłka %.0f mm" + +msgid " transitions %.0f m" +msgstr " krzywe przejściowe %.0f m" + +msgid "the shape chosen towards the cursor, to a free end: joins it" +msgstr "wybrany kształt w stronę kursora, na wolny koniec: łączy z nim" + +msgid "Ctrl+wheel" +msgstr "Ctrl+kółko" + +msgid "Shift+wheel" +msgstr "Shift+kółko" + +msgid "arc / straight, at a free end: another shape" +msgstr "łuk / prosta, na wolnym końcu: inny kształt" + +msgid "lay the shape up to here and go on from its end" +msgstr "kładzie kształt dotąd i idzie dalej od jego końca" + +msgid "type the radius" +msgstr "wpisz promień" From 55bb48e8bd29eaac773ec08037025c270ae49616 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 11:39:57 +0200 Subject: [PATCH 002/150] feat(editor): laying from the ground goes on like laying from a free end - the second click of the lay tool lays a straight from the first point and goes on from its end, each next click lays the shape chosen (arc then straight, straight then arc, arc through the cursor, straight) with the radius on Ctrl+wheel, as from a free end; no polyline of vertices cut into pieces of the longest path any more - a track started on the ground and ended at a free end is the shape pulled out of that end towards the start point, one path per up to 90 degrees of arc - hints and readout the same for both; self-test command layclicks --- application/editormodeselftest.cpp | 38 ++++++++++++++++++++++++++++++ application/editormodetrack.cpp | 35 +++++++++++++++++---------- lang/editor_pl.po | 15 +++--------- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 593ade8a..ae224e4a 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -90,6 +90,7 @@ http://mozilla.org/MPL/2.0/. // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given // join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other // layto from the free end at the first point towards the second, as a drag in the layout +// layclicks ... clicks of the lay tool at the points, a free end within 3 m joined // joints ... the joints check over the scenery, the issues around the points logged // screenshot // save @@ -1042,6 +1043,43 @@ void editor_mode::selftest_step() if (false == created.empty()) WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); } + else if (command == "layclicks") + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + auto const active{m_lay.active}; + m_lay.active = true; + lay_cancel(); + auto const before{m_history.size()}; + for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) + { + point.y = m_lay.points.empty() ? 0.0 : m_lay.points.back().y; + m_lay.mouse = point; + m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); + lay_click(); + WriteLog(format("SELFTEST layclicks %.1f %.1f: %s", point.x, point.z, m_lay.status.c_str())); + } + lay_cancel(); + m_lay.active = active; + glm::dvec2 heading{0.0}; + glm::dvec3 previous{0.0}; + bool first{true}; + for (auto step{before}; step < m_history.size(); ++step) + for (auto const *track : m_history[step].created) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), + first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a))); + heading = glm::normalize(geometry::plan_of(curve.first(1.0))); + previous = b; + first = false; + } + } else if (command == "joints") { std::vector points; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 7c021098..cdfd354a 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3495,13 +3495,13 @@ std::vector editor_mode::track_key_hints(bool const All) if (m_lay.active) { if (m_lay.points.empty()) - hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "straight along it, to another free end: a curve joining them"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; + hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "the shape chosen towards the cursor, to a free end: joins it"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; else { - bool const chained{m_lay.points.size() == 1 && m_lay.start.track != nullptr}; - hints = {{"LMB", chained ? "lay the shape up to here and go on from its end" : m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Ctrl+wheel", "radius"}}; + bool const chained{m_lay.start.track != nullptr}; + hints = {{"LMB", chained ? "lay the shape up to here and go on from its end" : "lay the straight up to here and go on from its end"}, {"LMB on a free end", "join and lay"}}; if (chained || m_lay.mouse_snap.track != nullptr) - hints.push_back({"Shift+wheel", "arc / straight, at a free end: another shape"}); + hints.insert(hints.end(), {{"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}}); hints.insert(hints.end(), {{"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}); if (glm::dvec2 heading; lay_heading(heading)) hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); @@ -3669,9 +3669,7 @@ std::string editor_mode::track_readout() const if (false == lay.preview_error.empty()) text += "\n" + lay.preview_error; if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr)) - text += "\n" + track_shaping(lay.mouse_snap.track != nullptr); - else if (false == lay.points.empty()) - text += "\n" + format(STR_C("R %.0f m Ctrl+wheel: radius"), lay.radius); + text += "\n" + track_shaping(lay.mouse_snap.track != nullptr && lay.start.track != nullptr); return text; } auto const &state{m_straights}; @@ -4509,7 +4507,7 @@ bool editor_mode::track_wheel(double const Offset) return false; int const step{Offset > 0.0 ? 1 : -1}; editor_track::snap_target target; - bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr}; + bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr && m_lay.start.track != nullptr}; static double const radii[]{100.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0, 4000.0, 6000.0}; auto const next_radius = [&](double const Current) { if (step > 0) @@ -4550,7 +4548,7 @@ std::vector editor_mode::lay_pieces(std::vector const Length = 0.0; if (Points.size() < 2) return {}; - if (Points.size() == 2 && (End.track != nullptr || tool.start.track != nullptr)) + if (Points.size() == 2) { auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; if (glm::length(leaving) < 1e-6) @@ -4558,10 +4556,23 @@ std::vector editor_mode::lay_pieces(std::vector const Error = STR_C("Two points lie on one another"); return {}; } - if (End.track != nullptr) + if (End.track != nullptr && tool.start.track != nullptr) return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); m_join_last = {}; - auto const pieces{layout_pieces(Points[0], glm::normalize(leaving), -grade_of(tool.start.direction), Points[1], tool.radius)}; + std::vector pieces; + if (tool.start.track != nullptr) + pieces = layout_pieces(Points[0], glm::normalize(leaving), -grade_of(tool.start.direction), Points[1], tool.radius); + else if (End.track == nullptr) + { + auto const length{glm::length(leaving)}; + pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length); + } + else if (glm::length(plan_of(End.direction)) > 1e-6) + { + auto const back{layout_pieces(End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; + for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) + pieces.push_back(turned_around(*piece)); + } for (auto const &piece : pieces) Length += bezier{piece}.plan_length(); if (pieces.empty()) @@ -4707,7 +4718,7 @@ void editor_mode::lay_click() if (plan_distance(point, tool.points.back()) < 1.0) return; m_typed.clear(); - if (tool.points.size() == 1 && tool.start.track != nullptr) + if (tool.points.size() == 1) { tool.points.push_back(tool.mouse); lay_finish({}); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 54e4f947..21cd94df 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -29,12 +29,6 @@ msgstr "Pokaż inspektor" msgid "The curve can't be read from this path, edit it as a single path" msgstr "Z tego odcinka nie da się odczytać łuku, edytuj go jako pojedynczy odcinek" -msgid "end of the straight, or a vertex of a curve" -msgstr "koniec prostej albo wierzchołek łuku" - -msgid "next vertex of the curve" -msgstr "kolejny wierzchołek łuku" - msgid "Laying track: LMB adds the points" msgstr "Układanie toru: LPM dodaje punkty" @@ -2934,9 +2928,6 @@ msgstr "Rozjazd: wybierz typ z listy" msgid "Drag from a free end" msgstr "Przeciągnij z wolnego końca" -msgid "straight along it, to another free end: a curve joining them" -msgstr "prosta wzdłuż niego, do innego wolnego końca: łuk łączący" - msgid "moves the point, level" msgstr "przesuwa punkt w poziomie" @@ -5236,9 +5227,6 @@ msgstr "Co przeciągnięcie lub kliknięcie z wolnego końca kładzie w stronę msgid "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows" msgstr "Prędkość nowych torów i prędkość projektowa krzywych przejściowych oraz przechyłki; pierwszeństwo ma prędkość toru, do którego się dołącza, i nigdy nie wyższa, niż pozwala promień" -msgid "R %.0f m Ctrl+wheel: radius" -msgstr "R %.0f m Ctrl+kółko: promień" - msgid "Ctrl+wheel: radius, Shift+wheel: arc / straight" msgstr "Ctrl+kółko: promień, Shift+kółko: łuk / prosta" @@ -5281,5 +5269,8 @@ msgstr "łuk / prosta, na wolnym końcu: inny kształt" msgid "lay the shape up to here and go on from its end" msgstr "kładzie kształt dotąd i idzie dalej od jego końca" +msgid "lay the straight up to here and go on from its end" +msgstr "kładzie prostą dotąd i idzie dalej od jej końca" + msgid "type the radius" msgstr "wpisz promień" From 3a549e76a9b27f927b9c629eebe53a771cc3ffc6 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 11:50:19 +0200 Subject: [PATCH 003/150] feat(editor): Shift no longer forces a straight while laying - laying, dragging out of a free end and the curve of a switch go on with the shape chosen; a straight is the straight layout on Shift+wheel, at a diverging end of a switch it also stops the crossover - hints without Shift: straight --- application/editormode.h | 3 --- application/editormodetrack.cpp | 44 ++++--------------------------- application/editormodetrackux.cpp | 10 +++---- lang/editor_pl.po | 10 ++----- 4 files changed, 12 insertions(+), 55 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 6944c986..9ef8f17b 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -966,7 +966,6 @@ class editor_mode : public application_mode, private editor_track::observer void lay_finish(editor_track::snap_target const &End); void lay_cancel(); void lay_enter(); - bool lay_heading(glm::dvec2 &Heading) const; std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; void render_switch_ui(); @@ -1019,11 +1018,9 @@ class editor_mode : public application_mode, private editor_track::observer TTrack const *m_join_track{nullptr}; editor_track::point_ref m_join_point; mutable alignment::join_result m_join_last; - bool m_shift_wheel{false}; bool track_wheel(double const Offset); std::string track_shaping(bool const Joined) const; void render_layout_combo(); - bool shift_straight() const; double design_speed(TTrack const *From, TTrack const *To) const; std::vector layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const; std::vector join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index cdfd354a..fbd43a3a 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3503,8 +3503,6 @@ std::vector editor_mode::track_key_hints(bool const All) if (chained || m_lay.mouse_snap.track != nullptr) hints.insert(hints.end(), {{"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}}); hints.insert(hints.end(), {{"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}); - if (glm::dvec2 heading; lay_heading(heading)) - hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); } } else if (false == m_straights.detour.empty()) @@ -3514,7 +3512,7 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}}; } else if (m_extend.active) - hints = {{"Drag", "the shape chosen towards the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}, {"Shift", "straight, joins nothing"}, {"Release", "build"}, {"Esc", "cancel"}}; + hints = {{"Drag", "the shape chosen towards the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_switch.placing) hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; else if (m_track_tab == track_tab::lay) @@ -3837,8 +3835,6 @@ void editor_mode::update_build_tools() auto const &io{ImGui::GetIO()}; glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; m_straights.detour_mouse = ground; - if (false == io.KeyShift) - m_shift_wheel = false; auto const joining = [&](editor_track::snap_target const &Target) { if (Target.track == m_join_track && Target.point == m_join_point) return; @@ -3853,7 +3849,7 @@ void editor_mode::update_build_tools() glm::dvec3 level; m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast(kMaxPlacementDistance) ? level : ground; typed_extend(); - m_extend.snap = m_typed.empty() && false == shift_straight() && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + m_extend.snap = m_typed.empty() && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; joining(m_extend.snap); } if (m_switch.placing) @@ -3872,16 +3868,8 @@ void editor_mode::update_build_tools() mouse = glm::dvec3{m_cursor_override->x, height, m_cursor_override->z}; else if (false == lay_projection.on_level(io.MousePos, height, mouse)) mouse = ground; - glm::dvec2 heading; - bool const straight{shift_straight() && lay_heading(heading)}; - if (straight) - { - auto const &last{lay.points.back()}; - auto const along{std::max(0.0, glm::dot(glm::dvec2{mouse.x - last.x, mouse.z - last.z}, heading))}; - mouse = {last.x + heading.x * along, mouse.y, last.z + heading.y * along}; - } lay.mouse = typed_lay(mouse); - lay.mouse_snap = straight ? editor_track::snap_target{} : m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); if (false == m_extend.active) joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); if (lay.points.empty()) @@ -4195,7 +4183,7 @@ std::vector editor_mode::extend_pieces() const return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); } m_join_last = {}; - if (false == shift_straight() && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) + if (m_track_layout != track_layout::straight && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) { auto const &diverging{tool.track->m_paths[1]}; bezier const curve{diverging}; @@ -4217,8 +4205,6 @@ std::vector editor_mode::extend_pieces() const return pieces; } } - if (shift_straight()) - return straight_piece(tool.point, tool.direction, tool.grade, glm::dot(glm::dvec2{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}, tool.direction)); auto radius{m_lay.radius}; if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) radius = std::abs(values[0]); @@ -4364,11 +4350,6 @@ std::vector editor_mode::join_pieces(TTrack const *From, editor_tr return result.pieces; } -bool editor_mode::shift_straight() const -{ - return ImGui::GetIO().KeyShift && false == m_shift_wheel; -} - void editor_mode::finish_extend() { auto const &io{ImGui::GetIO()}; @@ -4534,7 +4515,6 @@ bool editor_mode::track_wheel(double const Offset) m_join_radius = next_radius(m_join_radius); return true; } - m_shift_wheel = true; if (joined) m_join_variant += step; else @@ -4795,20 +4775,6 @@ void editor_mode::lay_finish(editor_track::snap_target const &End) straight_refresh(); } -bool editor_mode::lay_heading(glm::dvec2 &Heading) const -{ - auto const &tool{m_lay}; - glm::dvec2 direction{0.0}; - if (tool.points.size() >= 2) - direction = plan_of(tool.points.back() - tool.points[tool.points.size() - 2]); - else if (tool.points.size() == 1 && tool.start.track != nullptr) - direction = -plan_of(tool.start.direction); - if (glm::length(direction) < 1e-6) - return false; - Heading = glm::normalize(direction); - return true; -} - void editor_mode::lay_enter() { auto &tool{m_lay}; @@ -5745,7 +5711,7 @@ void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &En bool editor_mode::plan_crossover(crossover_plan &Plan) const { auto const &tool{m_extend}; - if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || ImGui::GetIO().KeyShift) + if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || m_track_layout == track_layout::straight) return false; auto const &own{*tool.track}; auto const &main{own.m_paths[0]}; diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 158172e4..6e0cec3a 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -212,7 +212,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) intent.position = lay.mouse_snap.position; auto const diverging{lay.mouse_snap.track->eType == tt_Switch && lay.mouse_snap.point == editor_track::point_ref{1, editor_track::point_kind::end}}; if (lay.points.empty() && diverging) - return make(kind::lay_end, "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight"); + return make(kind::lay_end, "Click: start from this free end, drag: the curve of the switch goes on"); return make(kind::lay_end, lay.points.empty() ? "Click: start from this free end, drag: lead a track out of it" : "Click: join this end and lay"); } intent.position = lay.mouse; @@ -288,7 +288,7 @@ editor_mode::track_intent editor_mode::track_intent_at(int const Mods) intent.snap.point = hit; intent.snap.position = intent.position; auto const diverging{selected->eType == tt_Switch && hit == editor_track::point_ref{1, editor_track::point_kind::end}}; - return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on, Shift: straight; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end"); + return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end"); } } } @@ -731,7 +731,7 @@ char const *editor_mode::typed_meaning() const { auto const &straights{m_straights}; if (m_extend.active) - return m_track_layout == track_layout::straight || shift_straight() ? "type the length" : m_track_layout == track_layout::arc ? "type the radius, a space, the length" : "type the radius"; + return m_track_layout == track_layout::straight ? "type the length" : m_track_layout == track_layout::arc ? "type the radius, a space, the length" : "type the radius"; if (straights.dragging) return straights.handle == 2 ? "type the shift, + to the right" : "type the length"; if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging)) @@ -800,9 +800,9 @@ void editor_mode::typed_extend() return; auto &tool{m_extend}; auto const &direction{tool.direction}; - if (m_track_layout != track_layout::straight && m_track_layout != track_layout::arc && false == shift_straight()) + if (m_track_layout != track_layout::straight && m_track_layout != track_layout::arc) return; - if (m_track_layout == track_layout::straight || shift_straight()) + if (m_track_layout == track_layout::straight) { auto const length{std::max(0.0, values[0])}; tool.mouse = {tool.point.x + direction.x * length, tool.mouse.y, tool.point.z + direction.y * length}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 21cd94df..e95c4750 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -2982,12 +2982,6 @@ msgstr "ułóż do kursora" msgid "Shift" msgstr "Shift" -msgid "straight on in the direction of the track" -msgstr "prosto w kierunku toru" - -msgid "straight, joins nothing" -msgstr "prosto, bez łączenia" - msgid "moves the whole track" msgstr "przesuwa cały tor" @@ -3360,8 +3354,8 @@ msgstr "Wzdłuż trasy, tam gdzie go brak, powstają kawałki terenu edytora z w msgid "Puts back the terrain as it was before the last shaping, and takes away the terrain it made" msgstr "Przywraca teren sprzed ostatniego kształtowania i usuwa teren, który przy tym powstał" -msgid "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight" -msgstr "Kliknij: zacznij od tego wolnego końca, przeciągnij: łuk rozjazdu biegnie dalej, Shift: prosta" +msgid "Click: start from this free end, drag: the curve of the switch goes on" +msgstr "Kliknij: zacznij od tego wolnego końca, przeciągnij: łuk rozjazdu biegnie dalej" msgid "%s and %d vehicle(s) behind it stand on %s. It isn't saved with the scenery" msgstr "%s i %d pojazd(ów) za nim stoją na %s. Nie zapisuje się ze scenerią" From cfc2b2c64a9be95deca34696b44815bdfd4c8870 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 11:58:46 +0200 Subject: [PATCH 004/150] feat(editor): laid track stands on the ground with the foot of its ballast - each click of the lay tool and the end of a drag out of a free end take the height of the ground under the lower foot of the ballast plus its depth, the rails never under the ground at the axis - heights between the clicks follow a curve which keeps the grade of the track laid from at the joint and leaves with the grade of the chord, onto a free end with the grade of that track; no grade limits - the cursor follows the ground along its ray instead of the level of the last point - self-test layclicks: heights of the ground and the track, grades and grade breaks at the joints --- application/editormode.h | 3 + application/editormodeselftest.cpp | 16 +++- application/editormodetrack.cpp | 130 +++++++++++++++++++++++++---- 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 9ef8f17b..8050f2fc 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1144,6 +1144,9 @@ class editor_mode : public application_mode, private editor_track::observer bool switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point); glm::dvec3 cursor_ground() const; glm::dvec3 cursor_level(double const Height) const; + glm::dvec3 cursor_terrain(double const Height); + double bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style); + glm::dvec3 lay_bed(glm::dvec3 const &Point); std::optional m_cursor_override; bool sweep_grabs(glm::dvec3 const &Point); // menu of the path a click of the right mouse button lands on diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index ae224e4a..6b3fbf80 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1054,9 +1054,13 @@ void editor_mode::selftest_step() auto const before{m_history.size()}; for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) { - point.y = m_lay.points.empty() ? 0.0 : m_lay.points.back().y; - m_lay.mouse = point; + point.y = 5000.0; + std::vector found; + auto const ground{ground_heights({point}, true, &found)}; + point.y = found[0] != 0 ? ground[0] : m_lay.points.empty() ? 0.0 : m_lay.points.back().y; m_lay.mouse_snap = editor_track::find_free_end(nullptr, 1, point, 3.0, {}); + m_lay.mouse = m_lay.mouse_snap.track != nullptr ? point : lay_bed(point); + WriteLog(format("SELFTEST layclicks ground %.3f, track %.3f", point.y, m_lay.mouse.y)); lay_click(); WriteLog(format("SELFTEST layclicks %.1f %.1f: %s", point.x, point.z, m_lay.status.c_str())); } @@ -1064,6 +1068,7 @@ void editor_mode::selftest_step() m_lay.active = active; glm::dvec2 heading{0.0}; glm::dvec3 previous{0.0}; + double slope{0.0}; bool first{true}; for (auto step{before}; step < m_history.size(); ++step) for (auto const *track : m_history[step].created) @@ -1073,8 +1078,11 @@ void editor_mode::selftest_step() auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; auto const &a{path.points[segment_data::point::start]}; auto const &b{path.points[segment_data::point::end]}; - WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), - first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a))); + auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); }; + WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast(path.radius), + first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)), + first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope))); + slope = rise(curve.first(1.0)); heading = glm::normalize(geometry::plan_of(curve.first(1.0))); previous = b; first = false; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index fbd43a3a..382b69e0 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -201,6 +201,56 @@ std::vector straight_piece(glm::dvec3 const &Point, glm::dvec2 con return {path}; } +void follow_heights(std::vector &Pieces, double const Height0, double Grade0, double const Height1, double Grade1) +{ + std::vector lengths; + double total{0.0}; + for (auto const &piece : Pieces) + total += lengths.emplace_back(bezier{piece}.plan_length()); + if (total < 1e-3) + return; + auto const chord{(Height1 - Height0) / total}; + if (std::isnan(Grade0)) + Grade0 = chord; + if (std::isnan(Grade1)) + Grade1 = chord; + auto const height = [&](double const Chainage) { + auto const t{Chainage / total}; + return (2.0 * t * t * t - 3.0 * t * t + 1.0) * Height0 + (t * t * t - 2.0 * t * t + t) * total * Grade0 + (3.0 * t * t - 2.0 * t * t * t) * Height1 + (t * t * t - t * t) * total * Grade1; + }; + auto const grade = [&](double const Chainage) { + auto const t{Chainage / total}; + return (6.0 * t * t - 6.0 * t) * (Height0 - Height1) / total + (3.0 * t * t - 4.0 * t + 1.0) * Grade0 + (3.0 * t * t - 2.0 * t) * Grade1; + }; + double chainage{0.0}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + auto &points{Pieces[i].points}; + auto const length{lengths[i]}; + auto const from{grade(chainage)}; + auto const to{grade(chainage + length)}; + auto &start{points[segment_data::point::start]}; + auto &end{points[segment_data::point::end]}; + auto &control1{points[segment_data::point::control1]}; + auto &control2{points[segment_data::point::control2]}; + start.y = height(chainage); + end.y = height(chainage + length); + chainage += length; + if (control1 != glm::dvec3{} || control2 != glm::dvec3{}) + { + control1.y = from * glm::length(plan_of(control1)); + control2.y = -to * glm::length(plan_of(control2)); + continue; + } + auto const linear{(end.y - start.y) / std::max(length, 1e-6)}; + if (length < 1e-3 || (std::abs(from - linear) < 1e-6 && std::abs(to - linear) < 1e-6)) + continue; + auto const along{glm::normalize(plan_of(end - start)) * (length / 3.0)}; + control1 = {along.x, from * length / 3.0, along.y}; + control2 = {-along.x, -to * length / 3.0, -along.y}; + } +} + std::vector arc_through(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec2 const &Offset) { auto const along{glm::dot(Offset, Direction)}; @@ -3845,11 +3895,11 @@ void editor_mode::update_build_tools() }; if (m_extend.active) { - screen_projection const projection; - glm::dvec3 level; - m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast(kMaxPlacementDistance) ? level : ground; + m_extend.mouse = cursor_terrain(m_extend.point.y); typed_extend(); m_extend.snap = m_typed.empty() && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + if (m_extend.snap.track == nullptr) + m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track); joining(m_extend.snap); } if (m_switch.placing) @@ -3861,15 +3911,11 @@ void editor_mode::update_build_tools() if (false == lay.active) return; // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor - screen_projection const lay_projection; - double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : 0.0}; - glm::dvec3 mouse{ground}; - if (m_cursor_override.has_value()) - mouse = glm::dvec3{m_cursor_override->x, height, m_cursor_override->z}; - else if (false == lay_projection.on_level(io.MousePos, height, mouse)) - mouse = ground; - lay.mouse = typed_lay(mouse); + double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : ground.y}; + lay.mouse = typed_lay(cursor_terrain(height)); lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + if (lay.mouse_snap.track == nullptr) + lay.mouse = lay_bed(lay.mouse); if (false == m_extend.active) joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); if (lay.points.empty()) @@ -3966,6 +4012,52 @@ glm::dvec3 editor_mode::cursor_ground() const return {ground.x, 0.0, ground.z}; } +glm::dvec3 editor_mode::cursor_terrain(double const Height) +{ + auto point{cursor_level(Height)}; + for (int i = 0; i < 5; ++i) + { + std::vector found; + auto const heights{ground_heights({point}, true, &found)}; + if (found[0] == 0) + return point; + auto const next{cursor_level(heights[0])}; + auto const settled{plan_distance(next, point) < 0.01}; + point = {next.x, heights[0], next.z}; + if (settled) + break; + } + return point; +} + +double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) +{ + auto const [depth, half]{Style != nullptr ? profile_ballast(*Style) : std::pair{0.2, 0.5 * 1.435 + 0.5 + 1.1}}; + std::vector points{Point}; + if (glm::length(Direction) > 1e-6) + { + auto const side{glm::normalize(glm::dvec2{-Direction.y, Direction.x}) * half}; + points.push_back(Point + glm::dvec3{side.x, 0.0, side.y}); + points.push_back(Point - glm::dvec3{side.x, 0.0, side.y}); + } + std::vector found; + auto const heights{ground_heights(points, true, &found)}; + auto toe{std::numeric_limits::max()}; + for (std::size_t i = 1; i < points.size(); ++i) + if (found[i] != 0) + toe = std::min(toe, heights[i]); + if (found[0] == 0) + return toe < std::numeric_limits::max() ? toe + depth : Point.y; + return std::max(toe < std::numeric_limits::max() ? toe + depth : heights[0] + depth, heights[0]); +} + +glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point) +{ + auto const &tool{m_lay}; + TTrack const *style{tool.start.track != nullptr ? tool.start.track : tool.copy_selected ? selected_track() : nullptr}; + return {Point.x, bed_height(Point, tool.points.empty() ? glm::dvec2{0.0} : plan_of(Point - tool.points.back()), style), Point.z}; +} + glm::dvec3 editor_mode::cursor_level(double const Height) const { if (m_cursor_override.has_value()) @@ -4202,13 +4294,16 @@ std::vector editor_mode::extend_pieces() const auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; for (auto &piece : pieces) piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; + follow_heights(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN()); return pieces; } } auto radius{m_lay.radius}; if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) radius = std::abs(values[0]); - return layout_pieces(tool.point, tool.direction, tool.grade, tool.mouse, radius); + auto pieces{layout_pieces(tool.point, tool.direction, tool.grade, tool.mouse, radius)}; + follow_heights(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN()); + return pieces; } std::vector editor_mode::layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const @@ -4541,7 +4636,11 @@ std::vector editor_mode::lay_pieces(std::vector const m_join_last = {}; std::vector pieces; if (tool.start.track != nullptr) - pieces = layout_pieces(Points[0], glm::normalize(leaving), -grade_of(tool.start.direction), Points[1], tool.radius); + { + auto const grade{-grade_of(tool.start.direction)}; + pieces = layout_pieces(Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); + follow_heights(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN()); + } else if (End.track == nullptr) { auto const length{glm::length(leaving)}; @@ -4552,6 +4651,7 @@ std::vector editor_mode::lay_pieces(std::vector const auto const back{layout_pieces(End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) pieces.push_back(turned_around(*piece)); + follow_heights(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction)); } for (auto const &piece : pieces) Length += bezier{piece}.plan_length(); @@ -4691,10 +4791,10 @@ void editor_mode::lay_click() if (tool.points.empty()) { tool.start = snap; - tool.points.push_back(snap.track != nullptr ? snap.position : placement_on_ground(tool.mouse)); + tool.points.push_back(snap.track != nullptr ? snap.position : tool.mouse); return; } - auto const point{placement_on_ground(tool.mouse)}; + auto const &point{tool.mouse}; if (plan_distance(point, tool.points.back()) < 1.0) return; m_typed.clear(); From 6284f4e0d365f1cdbebe7174b8d9e6cd4254199e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 12:12:48 +0200 Subject: [PATCH 005/150] feat(editor): laid track follows the ground between the clicks - the height along the new track is a smooth curve through points on the ground under the foot of the ballast, added where the track would hang or sink more than 0.1 m, at least 10 m apart; the paths are cut there, each joint keeps one grade on both sides - the clicked ends and the free ends joined keep their height and grade as before; no grade limits - self-test layclicks: how far the foot of the ballast goes over and under the ground along the paths laid --- application/editormode.cpp | 4 +- application/editormode.h | 8 +- application/editormodeselftest.cpp | 14 ++ application/editormodetrack.cpp | 249 +++++++++++++++++++++-------- 4 files changed, 207 insertions(+), 68 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index d89334e4..98819f10 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -548,7 +548,7 @@ editor_mode::~editor_mode() editor_track::observe(nullptr); } -std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found) +std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found) const { std::vector heights; heights.reserve(Points.size()); @@ -2449,7 +2449,7 @@ editor_terrain *editor_mode::terrain_at(double X, double Z) return m_streamer.terrain_at(X, Z); } -std::vector editor_mode::active_terrains() +std::vector editor_mode::active_terrains() const { std::vector out; out.reserve(m_streamer.resident()); diff --git a/application/editormode.h b/application/editormode.h index 8050f2fc..2a40a3f0 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -414,7 +414,7 @@ class editor_mode : public application_mode, private editor_track::observer // the chunk of the terrain (if any) covering the given world point editor_terrain *terrain_at(double X, double Z); // the chunks of the terrain held in memory - std::vector active_terrains(); + std::vector active_terrains() const; // samples the selected model instance's geometry into the chunks of the terrain it covers, then removes it void capture_terrain(); bool m_terrain_open{false}; // the terrain is the field of work, chosen in the toolbar @@ -1145,7 +1145,9 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 cursor_ground() const; glm::dvec3 cursor_level(double const Height) const; glm::dvec3 cursor_terrain(double const Height); - double bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style); + double bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const; + std::vector bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const; + void follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const; glm::dvec3 lay_bed(glm::dvec3 const &Point); std::optional m_cursor_override; bool sweep_grabs(glm::dvec3 const &Point); @@ -2018,7 +2020,7 @@ class editor_mode : public application_mode, private editor_track::observer void road_profile(std::vector &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh); // height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height // Found: set for the points which have the ground under them - std::vector ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found = nullptr); + std::vector ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found = nullptr) const; void push_road_snapshot(editor_road::record Record); void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); road_tool m_roadtool; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 6b3fbf80..01e25791 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1070,6 +1070,8 @@ void editor_mode::selftest_step() glm::dvec3 previous{0.0}; double slope{0.0}; bool first{true}; + double hanging{0.0}, buried{0.0}; + int paths{0}; for (auto step{before}; step < m_history.size(); ++step) for (auto const *track : m_history[step].created) { @@ -1086,7 +1088,19 @@ void editor_mode::selftest_step() heading = glm::normalize(geometry::plan_of(curve.first(1.0))); previous = b; first = false; + auto const count{std::max(1, static_cast(std::ceil(curve.plan_length() / 2.0)))}; + for (int k = 0; k <= count; ++k) + { + auto const t{curve.parameter(curve.plan_length() * k / count)}; + auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()}; + if (std::isnan(target)) + continue; + hanging = std::max(hanging, curve.point(t).y - target); + buried = std::max(buried, target - curve.point(t).y); + } + ++paths; } + WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried)); } else if (command == "joints") { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 382b69e0..294ca341 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -201,54 +201,27 @@ std::vector straight_piece(glm::dvec3 const &Point, glm::dvec2 con return {path}; } -void follow_heights(std::vector &Pieces, double const Height0, double Grade0, double const Height1, double Grade1) +double hermite(double const Length, double const Height0, double const Grade0, double const Height1, double const Grade1, double const T, double &Grade) { - std::vector lengths; - double total{0.0}; - for (auto const &piece : Pieces) - total += lengths.emplace_back(bezier{piece}.plan_length()); - if (total < 1e-3) - return; - auto const chord{(Height1 - Height0) / total}; - if (std::isnan(Grade0)) - Grade0 = chord; - if (std::isnan(Grade1)) - Grade1 = chord; - auto const height = [&](double const Chainage) { - auto const t{Chainage / total}; - return (2.0 * t * t * t - 3.0 * t * t + 1.0) * Height0 + (t * t * t - 2.0 * t * t + t) * total * Grade0 + (3.0 * t * t - 2.0 * t * t * t) * Height1 + (t * t * t - t * t) * total * Grade1; - }; - auto const grade = [&](double const Chainage) { - auto const t{Chainage / total}; - return (6.0 * t * t - 6.0 * t) * (Height0 - Height1) / total + (3.0 * t * t - 4.0 * t + 1.0) * Grade0 + (3.0 * t * t - 2.0 * t) * Grade1; - }; - double chainage{0.0}; - for (std::size_t i = 0; i < Pieces.size(); ++i) + auto const t2{T * T}; + auto const t3{t2 * T}; + Grade = (6.0 * t2 - 6.0 * T) * (Height0 - Height1) / Length + (3.0 * t2 - 4.0 * T + 1.0) * Grade0 + (3.0 * t2 - 2.0 * T) * Grade1; + return (2.0 * t3 - 3.0 * t2 + 1.0) * Height0 + (t3 - 2.0 * t2 + T) * Length * Grade0 + (3.0 * t2 - 2.0 * t3) * Height1 + (t3 - t2) * Length * Grade1; +} + +segment_data part_of(segment_data const &Piece, double const From, double const To) +{ + bezier const curve{Piece}; + segment_data result{Piece}; + result.points[segment_data::point::start] = curve.point(From); + result.points[segment_data::point::end] = curve.point(To); + if (Piece.points[segment_data::point::control1] != glm::dvec3{} || Piece.points[segment_data::point::control2] != glm::dvec3{}) { - auto &points{Pieces[i].points}; - auto const length{lengths[i]}; - auto const from{grade(chainage)}; - auto const to{grade(chainage + length)}; - auto &start{points[segment_data::point::start]}; - auto &end{points[segment_data::point::end]}; - auto &control1{points[segment_data::point::control1]}; - auto &control2{points[segment_data::point::control2]}; - start.y = height(chainage); - end.y = height(chainage + length); - chainage += length; - if (control1 != glm::dvec3{} || control2 != glm::dvec3{}) - { - control1.y = from * glm::length(plan_of(control1)); - control2.y = -to * glm::length(plan_of(control2)); - continue; - } - auto const linear{(end.y - start.y) / std::max(length, 1e-6)}; - if (length < 1e-3 || (std::abs(from - linear) < 1e-6 && std::abs(to - linear) < 1e-6)) - continue; - auto const along{glm::normalize(plan_of(end - start)) * (length / 3.0)}; - control1 = {along.x, from * length / 3.0, along.y}; - control2 = {-along.x, -to * length / 3.0, -along.y}; + result.points[segment_data::point::control1] = curve.first(From) * ((To - From) / 3.0); + result.points[segment_data::point::control2] = -curve.first(To) * ((To - From) / 3.0); } + result.rolls = {glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast(From)), glm::mix(Piece.rolls[0], Piece.rolls[1], static_cast(To))}; + return result; } std::vector arc_through(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec2 const &Offset) @@ -4030,25 +4003,174 @@ glm::dvec3 editor_mode::cursor_terrain(double const Height) return point; } -double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) +std::vector editor_mode::bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const { auto const [depth, half]{Style != nullptr ? profile_ballast(*Style) : std::pair{0.2, 0.5 * 1.435 + 0.5 + 1.1}}; - std::vector points{Point}; - if (glm::length(Direction) > 1e-6) + std::vector probes; + probes.reserve(Points.size() * 3); + for (std::size_t i = 0; i < Points.size(); ++i) { - auto const side{glm::normalize(glm::dvec2{-Direction.y, Direction.x}) * half}; - points.push_back(Point + glm::dvec3{side.x, 0.0, side.y}); - points.push_back(Point - glm::dvec3{side.x, 0.0, side.y}); + auto const &direction{Directions[i]}; + auto const side{glm::length(direction) > 1e-6 ? glm::normalize(glm::dvec2{-direction.y, direction.x}) * half : glm::dvec2{0.0}}; + probes.push_back(Points[i]); + probes.push_back(Points[i] + glm::dvec3{side.x, 0.0, side.y}); + probes.push_back(Points[i] - glm::dvec3{side.x, 0.0, side.y}); } std::vector found; - auto const heights{ground_heights(points, true, &found)}; - auto toe{std::numeric_limits::max()}; - for (std::size_t i = 1; i < points.size(); ++i) - if (found[i] != 0) - toe = std::min(toe, heights[i]); - if (found[0] == 0) - return toe < std::numeric_limits::max() ? toe + depth : Point.y; - return std::max(toe < std::numeric_limits::max() ? toe + depth : heights[0] + depth, heights[0]); + auto const heights{ground_heights(probes, true, &found)}; + std::vector result(Points.size(), std::numeric_limits::quiet_NaN()); + for (std::size_t i = 0; i < Points.size(); ++i) + { + auto const axis{found[3 * i] != 0 ? heights[3 * i] : std::numeric_limits::quiet_NaN()}; + auto toe{std::numeric_limits::infinity()}; + for (std::size_t j = 3 * i + 1; j < 3 * i + 3; ++j) + if (found[j] != 0) + toe = std::min(toe, heights[j]); + if (std::isfinite(toe)) + result[i] = std::isnan(axis) ? toe + depth : std::max(toe + depth, axis); + else if (false == std::isnan(axis)) + result[i] = axis + depth; + } + return result; +} + +double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const +{ + auto const height{bed_heights({Point}, {Direction}, Style).front()}; + return std::isnan(height) ? Point.y : height; +} + +void editor_mode::follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const +{ + double constexpr step{2.0}; + double constexpr tolerance{0.1}; + double constexpr spacing{10.0}; + std::vector lengths; + double total{0.0}; + for (auto const &piece : Pieces) + total += lengths.emplace_back(bezier{piece}.plan_length(256)); + if (total < 1e-3) + return; + std::vector chainages; + std::vector points; + std::vector directions; + double reached{0.0}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + bezier const curve{Pieces[i]}; + auto const count{std::max(1, static_cast(std::ceil(lengths[i] / step)))}; + for (int k = (i == 0 ? 1 : 0); k < count; ++k) + { + auto const along{lengths[i] * k / count}; + auto const t{curve.parameter(along)}; + auto const chainage{reached + along}; + auto point{curve.point(t)}; + point.y = Height0 + (Height1 - Height0) * chainage / total + 50.0; + chainages.push_back(chainage); + points.push_back(point); + directions.push_back(plan_of(curve.first(t))); + } + reached += lengths[i]; + } + auto const targets{bed_heights(points, directions, Style)}; + std::vector knots{0.0, total}; + std::vector heights{Height0, Height1}; + auto const slopes = [&]() { + auto const last{knots.size() - 1}; + std::vector result(knots.size()); + for (std::size_t i = 0; i <= last; ++i) + { + auto const a{i == 0 ? 0 : i - 1}; + auto const b{i == last ? last : i + 1}; + result[i] = (heights[b] - heights[a]) / (knots[b] - knots[a]); + } + if (false == std::isnan(Grade0)) + result.front() = Grade0; + if (false == std::isnan(Grade1)) + result.back() = Grade1; + return result; + }; + auto slope{slopes()}; + auto const at = [&](double const Chainage, double &Grade) { + auto const next{static_cast(std::upper_bound(knots.begin(), knots.end(), Chainage) - knots.begin())}; + auto const i{std::clamp(next, 1, knots.size() - 1) - 1}; + auto const length{knots[i + 1] - knots[i]}; + return hermite(length, heights[i], slope[i], heights[i + 1], slope[i + 1], std::clamp((Chainage - knots[i]) / length, 0.0, 1.0), Grade); + }; + for (std::size_t pass = 0; pass < chainages.size(); ++pass) + { + auto worst{tolerance}; + auto pick{chainages.size()}; + for (std::size_t j = 0; j < chainages.size(); ++j) + { + if (std::isnan(targets[j])) + continue; + double grade; + auto const miss{std::abs(at(chainages[j], grade) - targets[j])}; + if (miss <= worst) + continue; + auto const next{std::upper_bound(knots.begin(), knots.end(), chainages[j])}; + if (next == knots.end() || *next - chainages[j] < spacing || chainages[j] - *std::prev(next) < spacing) + continue; + worst = miss; + pick = j; + } + if (pick == chainages.size()) + break; + auto const where{std::upper_bound(knots.begin(), knots.end(), chainages[pick]) - knots.begin()}; + knots.insert(knots.begin() + where, chainages[pick]); + heights.insert(heights.begin() + where, targets[pick]); + slope = slopes(); + } + std::vector result; + std::vector> spans; + double from{0.0}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + bezier const curve{Pieces[i]}; + auto const to{from + lengths[i]}; + double cut{from}; + double parameter{0.0}; + for (auto const knot : knots) + { + if (knot < cut + 0.5 || knot > to - 0.5) + continue; + auto const next{curve.parameter(knot - from)}; + result.push_back(part_of(Pieces[i], parameter, next)); + spans.emplace_back(cut, knot); + cut = knot; + parameter = next; + } + result.push_back(parameter > 0.0 ? part_of(Pieces[i], parameter, 1.0) : Pieces[i]); + spans.emplace_back(cut, to); + from = to; + } + for (std::size_t i = 0; i < result.size(); ++i) + { + auto &points{result[i].points}; + auto const [a, b]{spans[i]}; + auto const length{b - a}; + double begin, end; + auto &start{points[segment_data::point::start]}; + auto &finish{points[segment_data::point::end]}; + auto &control1{points[segment_data::point::control1]}; + auto &control2{points[segment_data::point::control2]}; + start.y = at(a, begin); + finish.y = at(b, end); + if (control1 != glm::dvec3{} || control2 != glm::dvec3{}) + { + control1.y = begin * glm::length(plan_of(control1)); + control2.y = -end * glm::length(plan_of(control2)); + continue; + } + auto const linear{(finish.y - start.y) / std::max(length, 1e-6)}; + if (length < 1e-3 || (std::abs(begin - linear) < 1e-6 && std::abs(end - linear) < 1e-6)) + continue; + auto const along{glm::normalize(plan_of(finish - start)) * (length / 3.0)}; + control1 = {along.x, begin * length / 3.0, along.y}; + control2 = {-along.x, -end * length / 3.0, -along.y}; + } + Pieces = std::move(result); } glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point) @@ -4294,7 +4416,7 @@ std::vector editor_mode::extend_pieces() const auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; for (auto &piece : pieces) piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; - follow_heights(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN()); + follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); return pieces; } } @@ -4302,7 +4424,7 @@ std::vector editor_mode::extend_pieces() const if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) radius = std::abs(values[0]); auto pieces{layout_pieces(tool.point, tool.direction, tool.grade, tool.mouse, radius)}; - follow_heights(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN()); + follow_ground(pieces, tool.point.y, tool.grade, tool.mouse.y, std::numeric_limits::quiet_NaN(), tool.track); return pieces; } @@ -4639,19 +4761,20 @@ std::vector editor_mode::lay_pieces(std::vector const { auto const grade{-grade_of(tool.start.direction)}; pieces = layout_pieces(Points[0], glm::normalize(leaving), grade, Points[1], tool.radius); - follow_heights(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN()); + follow_ground(pieces, Points[0].y, grade, Points[1].y, std::numeric_limits::quiet_NaN(), tool.start.track); } else if (End.track == nullptr) { auto const length{glm::length(leaving)}; pieces = straight_piece(Points[0], glm::normalize(leaving), (Points[1].y - Points[0].y) / length, length); + follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), Points[1].y, std::numeric_limits::quiet_NaN(), tool.copy_selected ? selected_track() : nullptr); } else if (glm::length(plan_of(End.direction)) > 1e-6) { auto const back{layout_pieces(End.position, glm::normalize(-plan_of(End.direction)), -grade_of(End.direction), Points[0], tool.radius)}; for (auto piece{back.rbegin()}; piece != back.rend(); ++piece) pieces.push_back(turned_around(*piece)); - follow_heights(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction)); + follow_ground(pieces, Points[0].y, std::numeric_limits::quiet_NaN(), End.position.y, grade_of(End.direction), End.track); } for (auto const &piece : pieces) Length += bezier{piece}.plan_length(); From 0f3004a64cc57ea53d917b40178864339e858ebd Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 12:19:57 +0200 Subject: [PATCH 006/150] fix(editor): no crash laying track with the mouse outside the window - with no position of the mouse the ray gave NaN and the ground under it was looked up in a grid cell out of range; the lay and drag tools keep the last cursor then, the ground is not asked about points which are not numbers --- application/editormode.cpp | 2 ++ application/editormodetrack.cpp | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 98819f10..be970a88 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -567,6 +567,8 @@ std::vector editor_mode::ground_heights(std::vector const &P for (std::size_t i = 0; i < Points.size(); ++i) { auto const &p = Points[i]; + if (false == std::isfinite(p.x) || false == std::isfinite(p.y) || false == std::isfinite(p.z)) + continue; std::pair const key{static_cast(std::floor(p.x / ground_tile_size)), static_cast(std::floor(p.z / ground_tile_size))}; auto &tile = ground_tiles[key]; bool const missing = (tile.grid == nullptr); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 294ca341..16de5cda 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3866,9 +3866,11 @@ void editor_mode::update_build_tools() m_join_radius = 0.0; m_join_variant = 0; }; + bool const pointing{m_cursor_override.has_value() || ImGui::IsMousePosValid()}; if (m_extend.active) { - m_extend.mouse = cursor_terrain(m_extend.point.y); + if (pointing) + m_extend.mouse = cursor_terrain(m_extend.point.y); typed_extend(); m_extend.snap = m_typed.empty() && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; if (m_extend.snap.track == nullptr) @@ -3885,7 +3887,8 @@ void editor_mode::update_build_tools() return; // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : ground.y}; - lay.mouse = typed_lay(cursor_terrain(height)); + if (pointing) + lay.mouse = typed_lay(cursor_terrain(height)); lay.mouse_snap = m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); if (lay.mouse_snap.track == nullptr) lay.mouse = lay_bed(lay.mouse); @@ -3988,7 +3991,7 @@ glm::dvec3 editor_mode::cursor_ground() const glm::dvec3 editor_mode::cursor_terrain(double const Height) { auto point{cursor_level(Height)}; - for (int i = 0; i < 5; ++i) + for (int i = 0; i < 5 && std::isfinite(point.x) && std::isfinite(point.z); ++i) { std::vector found; auto const heights{ground_heights({point}, true, &found)}; From 2beae153907967db92965145e94c1c60f6dddabb Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 12:32:03 +0200 Subject: [PATCH 007/150] feat(editor): laying with no ground under the cursor takes the height of the ground around - with nothing under the cursor the height is the mean of the ground found on the nearest ring around the point, 10 to 400 m out, plus the depth of the ballast, instead of 0; the ray of the cursor meets that height - the ground under the cursor is looked for up to the height of the camera, not only 50 m over the last point --- application/editormode.h | 2 ++ application/editormodetrack.cpp | 47 +++++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 2a40a3f0..d3b35297 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1146,6 +1146,8 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 cursor_level(double const Height) const; glm::dvec3 cursor_terrain(double const Height); double bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const; + double nearby_ground(glm::dvec3 const &Point) const; + static std::pair trackbed(TTrack const *Style); std::vector bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const; void follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const; glm::dvec3 lay_bed(glm::dvec3 const &Point); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 16de5cda..7b6ea0b9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3994,21 +3994,55 @@ glm::dvec3 editor_mode::cursor_terrain(double const Height) for (int i = 0; i < 5 && std::isfinite(point.x) && std::isfinite(point.z); ++i) { std::vector found; - auto const heights{ground_heights({point}, true, &found)}; + auto ground{ground_heights({glm::dvec3{point.x, std::max(point.y, static_cast(Global.pCamera.Pos.y)), point.z}}, true, &found).front()}; if (found[0] == 0) + ground = nearby_ground(point); + if (std::isnan(ground)) return point; - auto const next{cursor_level(heights[0])}; + auto const next{cursor_level(ground)}; auto const settled{plan_distance(next, point) < 0.01}; - point = {next.x, heights[0], next.z}; + point = {next.x, ground, next.z}; if (settled) break; } return point; } +double editor_mode::nearby_ground(glm::dvec3 const &Point) const +{ + auto const ceiling{std::max(Point.y, static_cast(Global.pCamera.Pos.y))}; + for (double const radius : {10.0, 25.0, 50.0, 100.0, 200.0, 400.0}) + { + std::vector probes; + for (int i = 0; i < 12; ++i) + { + auto const angle{glm::two_pi() * i / 12.0}; + probes.push_back({Point.x + radius * std::cos(angle), ceiling, Point.z + radius * std::sin(angle)}); + } + std::vector found; + auto const heights{ground_heights(probes, true, &found)}; + double sum{0.0}; + int count{0}; + for (std::size_t i = 0; i < probes.size(); ++i) + if (found[i] != 0) + { + sum += heights[i]; + ++count; + } + if (count > 0) + return sum / count; + } + return std::numeric_limits::quiet_NaN(); +} + +std::pair editor_mode::trackbed(TTrack const *Style) +{ + return Style != nullptr ? profile_ballast(*Style) : std::pair{0.2, 0.5 * 1.435 + 0.5 + 1.1}; +} + std::vector editor_mode::bed_heights(std::vector const &Points, std::vector const &Directions, TTrack const *Style) const { - auto const [depth, half]{Style != nullptr ? profile_ballast(*Style) : std::pair{0.2, 0.5 * 1.435 + 0.5 + 1.1}}; + auto const [depth, half]{trackbed(Style)}; std::vector probes; probes.reserve(Points.size() * 3); for (std::size_t i = 0; i < Points.size(); ++i) @@ -4040,7 +4074,10 @@ std::vector editor_mode::bed_heights(std::vector const &Poin double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direction, TTrack const *Style) const { auto const height{bed_heights({Point}, {Direction}, Style).front()}; - return std::isnan(height) ? Point.y : height; + if (false == std::isnan(height)) + return height; + auto const nearby{nearby_ground(Point)}; + return std::isnan(nearby) ? Point.y : nearby + trackbed(Style).first; } void editor_mode::follow_ground(std::vector &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const From 89678ead3ee8ae54673ab0b2c6ad9a20340834b5 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 12:59:55 +0200 Subject: [PATCH 008/150] feat(editor): track gizmo turns, lengthens and edits the control vectors, joined tracks keep the tangent W turns the track (about its middle or the selected end) and a selection of tracks, the joined tracks follow. E lengthens the track at its free end: a straight stays straight, an arc keeps its radius, cant and grade. The gizmo on a control point moves, turns or lengthens the vector; the control point of the track joined there turns with it, at a switch the vector only slides along the switch. Straight neighbours dragged along keep their tangents and become straight again when nothing bent them. --- application/editormode.h | 35 +++ application/editormodeselftest.cpp | 132 +++++++++ application/editormodetrack.cpp | 456 +++++++++++++++++++++++++---- application/editormodetrackux.cpp | 130 +++++--- application/editorprojection.h | 4 +- editor/editorTrack.cpp | 83 ++++++ editor/editorTrack.hpp | 5 + lang/editor_pl.po | 36 +++ 8 files changed, 782 insertions(+), 99 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index d3b35297..1f3fc688 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -638,6 +638,36 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 m_track_pivot{0.0}; glm::mat4 m_track_gizmo{1.0f}; bool m_track_gizmo_using{false}; + struct track_gizmo_drag + { + gizmo_operation operation{gizmo_operation::translate}; + bool control{false}; + double angle{0.0}; + int path{-1}; + bool atend{true}; + std::size_t pieces{1}; + std::vector paths; + editor_track::point_ref point; + glm::dvec3 origin{0.0}; + glm::dvec3 axis{0.0}; + double grabbed{0.0}; + double length{0.0}; + double stretch{0.0}; + std::vector> partners; + glm::dvec3 fixed{0.0}; + std::vector>> held; + }; + track_gizmo_drag m_track_gizmo_drag; + gizmo_operation track_gizmo_operation(TTrack const &Track, bool const Pointmode) const; + glm::mat4 track_gizmo_frame(TTrack const &Track, bool const Pointmode, gizmo_operation const Operation) const; + void track_gizmo_hold(); + void track_gizmo_settle(); + void track_gizmo_stretch(TTrack &Track, double const Length); + void track_gizmo_control(TTrack &Track, glm::dvec3 Control); + void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); + void track_gizmo_turn(TTrack &Track, double const Angle); + void track_gizmo_length(TTrack &Track, double Length); + void track_gizmo_end(TTrack &Track); bool m_track_dirty{false}; std::chrono::steady_clock::time_point m_track_last_commit; editor_track::state m_track_field_before; @@ -1204,6 +1234,11 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 m_track_set_pivot{0.0}; std::vector>> m_track_set_joints; bool in_track_set(TTrack const *Track) const; + bool track_set_begin(bool const Rotate); + void track_set_turn(double const Angle); + void track_set_move(glm::dvec3 const &Offset); + void track_set_end(); + glm::dvec3 track_set_centre() const; void track_set_toggle(TTrack &Track); void track_set_prune(); void start_track_box(); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 01e25791..9e4e29e5 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1127,6 +1127,138 @@ void editor_mode::selftest_step() } WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); } + else if (command == "gizmo") + { + std::string action; + words >> action; + auto *track{selected_track()}; + if (track == nullptr) + { + WriteLog("SELFTEST gizmo: nothing selected"); + continue; + } + if (action == "set") + { + m_track_set.clear(); + for (std::string name; words >> name;) + if (auto *each{simulation::Paths.find(name)}; each != nullptr) + m_track_set.push_back(each); + m_track_set_pivot = track_set_centre(); + WriteLog(format("SELFTEST gizmo set: %zu tracks, centre %.3f %.3f", m_track_set.size(), m_track_set_pivot.x, m_track_set_pivot.z)); + } + else if (action == "setturn") + { + double degrees{0.0}; + words >> degrees; + if (track_set_begin(true)) + { + track_set_turn(glm::radians(degrees)); + track_set_end(); + } + } + else if (action == "op") + { + std::string operation; + words >> operation; + choose_work_area(work_area::tracks); + m_gizmo_op = operation == "rotate" ? gizmo_operation::rotate : operation == "scale" ? gizmo_operation::scale : gizmo_operation::translate; + int path{-1}; + std::string kind; + words >> path >> kind; + m_track_point = {}; + if (path >= 0) + m_track_point = {path, kind == "c1" ? editor_track::point_kind::control1 : kind == "c2" ? editor_track::point_kind::control2 : kind == "end" ? editor_track::point_kind::end : editor_track::point_kind::start}; + } + else if (action == "begin") + { + std::string operation, kind; + int path{-1}; + words >> operation >> path >> kind; + m_track_point = {}; + if (path >= 0) + m_track_point = {path, kind == "c1" ? editor_track::point_kind::control1 : kind == "c2" ? editor_track::point_kind::control2 : kind == "end" ? editor_track::point_kind::end : editor_track::point_kind::start}; + auto const wanted{operation == "rotate" ? gizmo_operation::rotate : operation == "scale" ? gizmo_operation::scale : gizmo_operation::translate}; + auto const previous{m_gizmo_op}; + m_gizmo_op = wanted; + auto const chosen{track_gizmo_operation(*track, m_track_point.valid())}; + m_gizmo_op = previous; + track_gizmo_begin(*track, m_track_point.valid(), chosen); + WriteLog(format("SELFTEST gizmo begin %s: %s, %zu tracks dragged, %zu joints, %zu partners%s", operation.c_str(), chosen == gizmo_operation::rotate ? "rotate" : chosen == gizmo_operation::scale ? "scale" : "translate", m_track_drag.size(), m_track_joints.size(), + m_track_gizmo_drag.partners.size(), m_track_gizmo_drag.fixed != glm::dvec3{} ? ", fixed direction" : "")); + } + else if (action == "turn") + { + double degrees{0.0}; + words >> degrees; + track_gizmo_turn(*track, glm::radians(degrees)); + } + else if (action == "length") + { + double length{0.0}; + words >> length; + track_gizmo_length(*track, length); + WriteLog(format("SELFTEST gizmo length %.3f of %.3f, %zu pieces", m_track_gizmo_drag.stretch, m_track_gizmo_drag.length, m_track_gizmo_drag.pieces)); + } + else if (action == "control") + { + glm::dvec3 control{0.0}; + words >> control.x >> control.y >> control.z; + track_gizmo_control(*track, control); + } + else if (action == "offset") + { + glm::dvec3 offset{0.0}; + words >> offset.x >> offset.y >> offset.z; + if (m_track_point.valid()) + { + auto const position{editor_track::point_position(*track, m_track_point) + offset}; + editor_track::move_point(*track, m_track_point, position); + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, position); + } + else + { + editor_track::translate(*track, offset); + editor_track::follow(*track, m_track_joints); + } + m_track_dirty = true; + } + else if (action == "end") + track_gizmo_end(*track); + else if (action == "show") + { + std::vector shown{track}; + for (auto *neighbour : editor_track::neighbours(*track)) + shown.push_back(neighbour); + WriteLog(format("SELFTEST gizmo %s prev %s next %s", track->name().c_str(), track->trPrev != nullptr ? track->trPrev->name().c_str() : "-", track->trNext != nullptr ? track->trNext->name().c_str() : "-")); + for (auto *each : shown) + for (int i = 0; i < static_cast(each->m_paths.size()); ++i) + { + auto const &path{each->m_paths[i]}; + auto const &a{path.points[segment_data::point::start]}; + auto const &b{path.points[segment_data::point::end]}; + auto const &c1{path.points[segment_data::point::control1]}; + auto const &c2{path.points[segment_data::point::control2]}; + WriteLog(format("SELFTEST gizmo path %s[%d] %.3f %.3f %.3f -> %.3f %.3f %.3f, c1 %.3f %.3f %.3f, c2 %.3f %.3f %.3f, L %.3f, R %.1f, rolls %.2f %.2f", each->name().c_str(), i, a.x, a.y, a.z, b.x, b.y, b.z, c1.x, c1.y, c1.z, c2.x, c2.y, c2.z, + geometry::bezier{path}.plan_length(), path.radius, path.rolls[0], path.rolls[1])); + } + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + geometry::bezier const own{track->m_paths[i]}; + bool const start{kind == editor_track::point_kind::start}; + auto const out{start ? -own.first(0.0) : own.first(1.0)}; + for (auto const &partner : editor_track::partners(*track, {i, kind})) + { + geometry::bezier const theirs{partner.first->m_paths[partner.second.path]}; + auto const in{partner.second.kind == editor_track::point_kind::start ? theirs.first(0.0) : -theirs.first(1.0)}; + auto const angle{glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(out), glm::normalize(in)), -1.0, 1.0)))}; + auto const gap{glm::distance(editor_track::point_position(*track, {i, kind}), editor_track::point_position(*partner.first, partner.second))}; + WriteLog(format("SELFTEST gizmo joint %s[%d].%s - %s[%d]: kink %.4f deg, gap %.4f m", track->name().c_str(), i, start ? "start" : "end", partner.first->name().c_str(), partner.second.path, angle, gap)); + } + } + } + } else if (command == "deletetrack") { delete_selected_track(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 7b6ea0b9..4b283efe 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -262,6 +262,137 @@ void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex) Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); } +std::vector stretched(segment_data const &Path, bool const Atend, double const Length) +{ + auto const path{Atend ? Path : turned_around(Path)}; + bezier const curve{path}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto const first{plan_of(curve.first(0.0))}; + auto const last{plan_of(curve.first(1.0))}; + if (glm::length(first) < 1e-9 || glm::length(last) < 1e-9) + return {}; + glm::dvec2 const direction{glm::normalize(first)}; + auto const grade{grade_of(curve.first(0.0))}; + auto const original{curve.plan_length()}; + auto const turn{signed_angle(direction, glm::normalize(last))}; + auto const lateral{cross(direction, plan_of(end - start))}; + std::vector pieces; + float radius{0.0f}; + if (std::abs(turn) < 1e-4 && std::abs(lateral) < 0.01) + pieces = straight_piece(start, direction, grade, Length); + else + { + auto const arcradius{original / std::abs(turn)}; + int const side{turn > 0.0 ? 1 : -1}; + auto const arc{arc_pieces(start, direction, grade, arcradius, std::abs(turn), side)}; + if (std::abs(turn) > 1e-4 && plan_distance(arc.back().points[segment_data::point::end], end) < 0.05 && plan_distance(arc.front().points[segment_data::point::start], start) < 1e-6) + { + pieces = arc_pieces(start, direction, grade, arcradius, std::min(Length / arcradius, glm::radians(330.0)), side); + radius = path.radius; + } + else + { + auto const scale{Length / std::max(original, 1e-6)}; + segment_data piece{path}; + piece.points[segment_data::point::end] = start + (end - start) * scale; + piece.points[segment_data::point::control1] *= scale; + piece.points[segment_data::point::control2] *= scale; + piece.radius *= static_cast(scale); + return {Atend ? piece : turned_around(piece)}; + } + } + double chainage{0.0}; + for (auto &piece : pieces) + { + auto const piecelength{bezier{piece}.plan_length()}; + auto const from{static_cast(std::clamp(chainage / original, 0.0, 1.0))}; + auto const to{static_cast(std::clamp((chainage + piecelength) / original, 0.0, 1.0))}; + piece.rolls = {glm::mix(path.rolls[0], path.rolls[1], from), glm::mix(path.rolls[0], path.rolls[1], to)}; + piece.radius = radius; + chainage += piecelength; + } + if (false == Atend) + { + std::reverse(pieces.begin(), pieces.end()); + for (auto &piece : pieces) + piece = turned_around(piece); + } + return pieces; +} + +bool stretch_end(TTrack const &Track, editor_track::point_ref const &Point, int &Path, bool &Atend) +{ + if (Track.eType != tt_Normal || false == editor_track::is_supported(Track) || Track.m_paths.empty()) + return false; + auto const last{static_cast(Track.m_paths.size()) - 1}; + if (Point.valid()) + { + if (false == editor_track::is_end(Point.kind) || Point.path > last) + return false; + Path = Point.path; + Atend = Point.kind == editor_track::point_kind::end; + auto const position{editor_track::point_position(Track, Point)}; + for (int i = 0; i <= last; ++i) + if (i != Path && (plan_distance(Track.m_paths[i].points[segment_data::point::start], position) < geometry::same_point || plan_distance(Track.m_paths[i].points[segment_data::point::end], position) < geometry::same_point)) + return false; + return true; + } + bool const startfree{false == editor_track::is_connected(Track, {0, editor_track::point_kind::start})}; + bool const endfree{false == editor_track::is_connected(Track, {last, editor_track::point_kind::end})}; + Atend = endfree || false == startfree; + Path = Atend ? last : 0; + return true; +} + +std::pair stretch_axis(segment_data const &Path, bool const Atend) +{ + bezier const curve{Path}; + auto const tangent{Atend ? curve.first(1.0) : -curve.first(0.0)}; + return {Path.points[Atend ? segment_data::point::end : segment_data::point::start], glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{}}; +} + +bool mouse_along(glm::dvec3 const &Origin, glm::dvec3 const &Axis, double &Along) +{ + screen_projection const projection; + glm::dvec3 origin, direction; + projection.ray(ImGui::GetIO().MousePos, origin, direction); + auto const b{glm::dot(Axis, direction)}; + auto const denominator{1.0 - b * b}; + if (denominator < 1e-6) + return false; + auto const w{Origin - origin}; + Along = (b * glm::dot(direction, w) - glm::dot(Axis, w)) / denominator; + return true; +} + +glm::mat4 gizmo_along(glm::dvec3 const &Position, glm::dvec3 const &Axis) +{ + auto gizmo{glm::translate(glm::mat4(1.0f), glm::vec3(Position))}; + if (glm::length(Axis) < 1e-9) + return gizmo; + auto const z{glm::normalize(Axis)}; + auto x{glm::cross(glm::dvec3{0.0, 1.0, 0.0}, z)}; + x = glm::length(x) > 1e-6 ? glm::normalize(x) : glm::dvec3{1.0, 0.0, 0.0}; + auto const y{glm::cross(z, x)}; + gizmo[0] = glm::vec4(glm::vec3(x), 0.0f); + gizmo[1] = glm::vec4(glm::vec3(y), 0.0f); + gizmo[2] = glm::vec4(glm::vec3(z), 0.0f); + return gizmo; +} + +editor_track::point_ref stretch_remap(editor_track::point_ref Point, int const Path, std::size_t const Pieces) +{ + if (false == Point.valid() || Point.path < Path) + return Point; + auto const extra{static_cast(Pieces) - 1}; + if (Point.path > Path) + Point.path += extra; + else if (Point.kind == editor_track::point_kind::end || Point.kind == editor_track::point_kind::control2) + Point.path += extra; + return Point; +} + } TTrack *editor_mode::selected_track() const @@ -395,6 +526,214 @@ void editor_mode::draw_track_overlay() const } } +editor_mode::gizmo_operation editor_mode::track_gizmo_operation(TTrack const &Track, bool const Pointmode) const +{ + if (m_gizmo_op == gizmo_operation::translate || (Pointmode && false == editor_track::is_end(m_track_point.kind))) + return m_gizmo_op; + if (m_gizmo_op == gizmo_operation::rotate) + return gizmo_operation::rotate; + int path{0}; + bool atend{true}; + return stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend) ? gizmo_operation::scale : gizmo_operation::translate; +} + +glm::mat4 editor_mode::track_gizmo_frame(TTrack const &Track, bool const Pointmode, gizmo_operation const Operation) const +{ + auto anchor{Pointmode ? editor_track::point_position(Track, m_track_point) : editor_track::pivot(Track)}; + glm::dvec3 axis{0.0}; + if (Operation == gizmo_operation::scale) + { + int path{0}; + bool atend{true}; + if (Pointmode && false == editor_track::is_end(m_track_point.kind)) + axis = anchor - editor_track::point_position(Track, editor_track::end_of(m_track_point)); + else if (stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend)) + std::tie(anchor, axis) = stretch_axis(Track.m_paths[path], atend); + } + return gizmo_along(anchor - glm::dvec3{GfxRenderer->Camera_Position()}, axis); +} + +void editor_mode::track_gizmo_hold() +{ + auto &drag{m_track_gizmo_drag}; + drag.held.clear(); + for (auto *track : m_track_drag) + { + if (drag.operation == gizmo_operation::scale && false == drag.control && track == selected_track()) + continue; + auto held{editor_track::hold_controls(*track)}; + if (false == held.empty()) + drag.held.emplace_back(track, std::move(held)); + } +} + +void editor_mode::track_gizmo_settle() +{ + for (auto &entry : m_track_gizmo_drag.held) + editor_track::settle_controls(*entry.first, entry.second); + m_track_gizmo_drag.held.clear(); +} + +void editor_mode::track_gizmo_stretch(TTrack &Track, double const Length) +{ + auto &drag{m_track_gizmo_drag}; + auto const pieces{stretched(drag.paths[drag.path], drag.atend, Length)}; + if (pieces.empty()) + return; + auto paths{drag.paths}; + paths.erase(paths.begin() + drag.path); + paths.insert(paths.begin() + drag.path, pieces.begin(), pieces.end()); + Track.m_paths = std::move(paths); + drag.pieces = pieces.size(); + drag.stretch = Length; + if (drag.point.valid()) + m_track_point = stretch_remap(drag.point, drag.path, drag.pieces); + auto joints{m_track_joints}; + for (auto &joint : joints) + joint.own = stretch_remap(joint.own, drag.path, drag.pieces); + editor_track::follow(Track, joints); + m_track_dirty = true; +} + +void editor_mode::track_gizmo_control(TTrack &Track, glm::dvec3 Control) +{ + auto const &drag{m_track_gizmo_drag}; + if (drag.fixed != glm::dvec3{}) + Control = drag.fixed * std::max(0.5, glm::dot(Control, drag.fixed)); + if (glm::length(Control) < 0.5) + Control = glm::length(Control) > 1e-9 ? glm::normalize(Control) * 0.5 : drag.axis * 0.5; + editor_track::move_point(Track, drag.point, editor_track::point_position(Track, editor_track::end_of(drag.point)) + Control); + editor_track::align_controls(Track, drag.point, drag.partners); + m_track_dirty = true; +} + +void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation) +{ + bool const controlmode{Pointmode && false == editor_track::is_end(m_track_point.kind)}; + auto &drag{m_track_gizmo_drag}; + m_track_gizmo_using = true; + m_track_pivot = editor_track::pivot(Track); + m_track_drag = {&Track}; + m_track_drag_points.clear(); + m_track_joints.clear(); + m_track_snap = {}; + drag = {}; + drag.operation = Operation; + drag.control = controlmode; + drag.point = Pointmode ? m_track_point : editor_track::point_ref{}; + bool const whole{false == Pointmode || (false == controlmode && Operation != gizmo_operation::translate)}; + if (whole && m_track_drag_connected) + { + m_track_joints = editor_track::joints(Track); + for (auto const &joint : m_track_joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.emplace_back(joint.other); + } + if (Pointmode && false == controlmode && Operation == gizmo_operation::translate && m_track_drag_connected) + { + for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, m_track_point))) + { + std::string reason; + if (false == editor_track::can_edit_geometry(*connection.first, reason)) + continue; + m_track_drag_points.emplace_back(connection); + if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) + m_track_drag.emplace_back(connection.first); + } + } + if (controlmode) + { + for (auto const &partner : editor_track::partners(Track, editor_track::end_of(m_track_point))) + { + std::string reason; + auto const &path{partner.first->m_paths[partner.second.path]}; + if (partner.first->eType != tt_Normal || false == editor_track::can_edit_geometry(*partner.first, reason)) + { + bool const start{partner.second.kind == editor_track::point_kind::start}; + auto const theirs{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; + auto const chord{path.points[start ? segment_data::point::end : segment_data::point::start] - path.points[start ? segment_data::point::start : segment_data::point::end]}; + auto const along{theirs != glm::dvec3{} ? theirs : chord}; + if (glm::length(along) > 1e-9) + drag.fixed = -glm::normalize(along); + continue; + } + if (false == m_track_drag_connected && partner.first != &Track) + continue; + drag.partners.push_back(partner); + if (std::find(m_track_drag.begin(), m_track_drag.end(), partner.first) == m_track_drag.end()) + m_track_drag.emplace_back(partner.first); + } + } + std::vector> states; + for (auto *dragged : m_track_drag) + states.emplace_back(dragged, editor_track::capture(*dragged)); + push_track_snapshot(std::move(states)); + track_gizmo_hold(); + if (controlmode) + { + drag.origin = editor_track::point_position(Track, m_track_point); + auto const control{drag.origin - editor_track::point_position(Track, editor_track::end_of(m_track_point))}; + drag.length = glm::length(control); + drag.axis = drag.length > 1e-9 ? control / drag.length : glm::dvec3{0.0, 0.0, 1.0}; + } + else if (Operation == gizmo_operation::scale) + { + stretch_end(Track, drag.point, drag.path, drag.atend); + drag.paths = Track.m_paths; + std::tie(drag.origin, drag.axis) = stretch_axis(Track.m_paths[drag.path], drag.atend); + drag.length = bezier{Track.m_paths[drag.path]}.plan_length(); + } + drag.stretch = drag.length; +} + +void editor_mode::track_gizmo_turn(TTrack &Track, double const Angle) +{ + auto &drag{m_track_gizmo_drag}; + auto const step{std::remainder(Angle - drag.angle, 2.0 * glm::pi())}; + if (std::abs(step) < 1e-9 || (drag.control && drag.fixed != glm::dvec3{})) + return; + drag.angle += step; + if (drag.control) + { + auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(drag.point))}; + auto const plan{turned(plan_of(control), -step)}; + track_gizmo_control(Track, {plan.x, control.y, plan.y}); + return; + } + editor_track::rotate_y(Track, drag.point.valid() ? editor_track::point_position(Track, drag.point) : m_track_pivot, step); + editor_track::follow(Track, m_track_joints); + m_track_dirty = true; +} + +void editor_mode::track_gizmo_length(TTrack &Track, double Length) +{ + auto &drag{m_track_gizmo_drag}; + Length = std::max(drag.control ? 0.5 : 1.0, Length); + if (std::abs(Length - drag.stretch) < 1e-6) + return; + if (drag.control) + { + drag.stretch = Length; + track_gizmo_control(Track, drag.axis * Length); + } + else if (drag.path >= 0) + track_gizmo_stretch(Track, Length); +} + +void editor_mode::track_gizmo_end(TTrack &Track) +{ + if (m_track_snap.track != nullptr && m_track_gizmo_drag.operation == gizmo_operation::translate && m_track_point.valid()) + editor_track::snap_point(Track, m_track_point, m_track_snap, m_track_align_tangent); + track_gizmo_settle(); + m_track_dirty = true; + m_track_snap = {}; + commit_track_drag(true); + m_track_gizmo_using = false; + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); +} + void editor_mode::render_track_gizmo() { auto *track = selected_track(); @@ -405,54 +744,49 @@ void editor_mode::render_track_gizmo() return; } bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size()); + bool const controlmode = pointmode && false == editor_track::is_end(m_track_point.kind); gizmo_frame const frame; + auto &drag{m_track_gizmo_drag}; + auto const operation{m_track_gizmo_using ? drag.operation : track_gizmo_operation(*track, pointmode)}; if (false == m_track_gizmo_using) - { - auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track); - m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); - } + m_track_gizmo = track_gizmo_frame(*track, pointmode, operation); glm::mat4 delta(1.0f); - frame.manipulate(ImGuizmo::TRANSLATE, m_track_gizmo, m_gizmo_snap, &delta); + auto const gizmo{operation == gizmo_operation::rotate ? ImGuizmo::ROTATE_Y : operation == gizmo_operation::scale ? ImGuizmo::SCALE_Z : ImGuizmo::TRANSLATE}; + ImGuizmo::AllowAxisFlip(operation != gizmo_operation::scale); + frame.manipulate(gizmo, m_track_gizmo, operation == gizmo_operation::rotate ? 5.0f : operation == gizmo_operation::scale ? 0.0f : m_gizmo_snap, &delta, operation == gizmo_operation::scale ? ImGuizmo::LOCAL : ImGuizmo::WORLD); + ImGuizmo::AllowAxisFlip(true); if (ImGuizmo::IsUsing()) { if (false == m_track_gizmo_using) { - m_track_gizmo_using = true; - m_track_pivot = editor_track::pivot(*track); - m_track_drag = {track}; - m_track_drag_points.clear(); - m_track_joints.clear(); - m_track_snap = {}; - if (false == pointmode && m_track_drag_connected) - { - m_track_joints = editor_track::joints(*track); - for (auto const &joint : m_track_joints) - if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) - m_track_drag.emplace_back(joint.other); - } - if (pointmode && editor_track::is_end(m_track_point.kind) && m_track_drag_connected) - { - for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) - { - std::string neighbourreason; - if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) - continue; - m_track_drag_points.emplace_back(connection); - if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) - m_track_drag.emplace_back(connection.first); - } - } - std::vector> states; - for (auto *dragged : m_track_drag) - states.emplace_back(dragged, editor_track::capture(*dragged)); - push_track_snapshot(std::move(states)); + track_gizmo_begin(*track, pointmode, operation); + if (operation == gizmo_operation::scale && false == mouse_along(drag.origin, drag.axis, drag.grabbed)) + drag.grabbed = 0.0; } - if (pointmode) + if (operation == gizmo_operation::rotate) + track_gizmo_turn(*track, std::atan2(static_cast(m_track_gizmo[2][0]), static_cast(m_track_gizmo[0][0]))); + else if (operation == gizmo_operation::scale) + { + double along{0.0}; + if (mouse_along(drag.origin, drag.axis, along)) + { + auto length{drag.length + along - drag.grabbed}; + if (Global.ctrlState && m_gizmo_snap > 0.0f) + length = std::round(length / m_gizmo_snap) * m_gizmo_snap; + track_gizmo_length(*track, length); + } + } + else if (controlmode) + { + glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); + track_gizmo_control(*track, position - editor_track::point_position(*track, editor_track::end_of(m_track_point))); + } + else if (pointmode) { glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); editor_track::move_point(*track, m_track_point, position); @@ -480,19 +814,7 @@ void editor_mode::render_track_gizmo() commit_track_drag(false); } else if (m_track_gizmo_using) - { - if (m_track_snap.track != nullptr && pointmode) - { - editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); - m_track_dirty = true; - } - m_track_snap = {}; - commit_track_drag(true); - m_track_gizmo_using = false; - m_track_drag.clear(); - m_track_drag_points.clear(); - m_track_joints.clear(); - } + track_gizmo_end(*track); } void editor_mode::commit_track_drag(bool const Force) @@ -3605,12 +3927,24 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}}; else if (m_track_point.valid()) { - hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the point along the axes"}, {"Esc", "releases the point"}}; + char const *action{"moves the point along the axes"}; + if (false == editor_track::is_end(m_track_point.kind)) + action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent"; + else if (m_gizmo_op == gizmo_operation::rotate) + action = "turns the track about the point, the joined tracks follow"; + else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, true) == gizmo_operation::scale) + action = "lengthens the track at this end, the curve keeps its radius"; + hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Esc", "releases the point"}}; gizmo = true; } else { - hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the whole track"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}}; + char const *action{"moves the whole track"}; + if (m_gizmo_op == gizmo_operation::rotate) + action = "turns the track, the joined tracks follow"; + else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, false) == gizmo_operation::scale) + action = "lengthens the track at its free end, the curve keeps its radius"; + hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}}; gizmo = true; } if (auto const *meaning{typed_meaning()}; meaning != nullptr) @@ -3736,9 +4070,31 @@ std::string editor_mode::track_readout() const return format("L %.3f m (%+.3f) azimuth %.4f deg", plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length, azimuth(state.preview_start, state.preview_end)); } + if (m_track_set_using && m_track_gizmo_drag.operation == gizmo_operation::rotate) + return format(STR_C("turned %+.2f deg"), glm::degrees(m_track_gizmo_drag.angle)); auto const *track{selected_track()}; if (m_track_gizmo_using && track != nullptr) { + auto const &drag{m_track_gizmo_drag}; + if (drag.control) + { + auto const control{editor_track::point_position(*track, m_track_point) - editor_track::point_position(*track, editor_track::end_of(m_track_point))}; + auto text{format(STR_C("control %.2f m azimuth %.2f deg"), glm::length(control), azimuth(glm::dvec3{0.0}, control))}; + if (drag.operation == gizmo_operation::rotate) + text += format(STR_C(" turned %+.2f deg"), glm::degrees(drag.angle)); + if (drag.fixed != glm::dvec3{}) + text += "\n" + STR("along the switch"); + return text; + } + if (drag.operation == gizmo_operation::scale && drag.path >= 0) + { + auto text{format("L %.3f m (%+.3f)", drag.stretch, drag.stretch - drag.length)}; + if (drag.path < static_cast(drag.paths.size()) && std::abs(drag.paths[drag.path].radius) > 0.5f) + text += format(" R %.1f m", std::abs(drag.paths[drag.path].radius)); + return text; + } + if (drag.operation == gizmo_operation::rotate) + return format(STR_C("turned %+.2f deg"), glm::degrees(drag.angle)); if (m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size())) { auto const &path{track->m_paths[m_track_point.path]}; diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 6e0cec3a..f478186b 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -1281,6 +1281,79 @@ void editor_mode::render_track_set_ui() ImGui::Separator(); } +bool editor_mode::track_set_begin(bool const Rotate) +{ + std::string reason; + if (false == tracks_editable(m_track_set, reason)) + { + ui()->set_status(reason, true); + return false; + } + m_track_set_using = true; + m_track_gizmo_drag = {}; + m_track_gizmo_drag.operation = Rotate ? gizmo_operation::rotate : gizmo_operation::translate; + m_track_drag = m_track_set; + m_track_set_joints.clear(); + for (auto *track : m_track_set) + { + auto joints{editor_track::joints(*track)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return in_track_set(Joint.other); }), joints.end()); + for (auto const &joint : joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.push_back(joint.other); + m_track_set_joints.emplace_back(track, std::move(joints)); + } + std::vector> states; + for (auto *track : m_track_drag) + states.emplace_back(track, editor_track::capture(*track)); + push_track_snapshot(std::move(states)); + track_gizmo_hold(); + return true; +} + +void editor_mode::track_set_turn(double const Angle) +{ + auto &drag{m_track_gizmo_drag}; + auto const step{std::remainder(Angle - drag.angle, 2.0 * glm::pi())}; + if (std::abs(step) < 1e-9) + return; + drag.angle += step; + for (auto &entry : m_track_set_joints) + editor_track::rotate_y(*entry.first, m_track_set_pivot, step); + for (auto &entry : m_track_set_joints) + editor_track::follow(*entry.first, entry.second); + m_track_dirty = true; +} + +void editor_mode::track_set_move(glm::dvec3 const &Offset) +{ + for (auto &entry : m_track_set_joints) + { + editor_track::translate(*entry.first, Offset); + editor_track::follow(*entry.first, entry.second); + } + m_track_dirty = true; +} + +void editor_mode::track_set_end() +{ + track_gizmo_settle(); + m_track_dirty = true; + commit_track_drag(true); + m_track_set_using = false; + m_track_drag.clear(); + m_track_set_joints.clear(); + straight_refresh(); +} + +glm::dvec3 editor_mode::track_set_centre() const +{ + glm::dvec3 centre{0.0}; + for (auto const *track : m_track_set) + centre += editor_track::pivot(*track); + return m_track_set.empty() ? centre : centre / static_cast(m_track_set.size()); +} + void editor_mode::render_track_set_gizmo() { track_set_prune(); @@ -1292,61 +1365,24 @@ void editor_mode::render_track_set_gizmo() gizmo_frame const frame; if (false == m_track_set_using) { - glm::dvec3 centre{0.0}; - for (auto const *track : m_track_set) - centre += editor_track::pivot(*track); - m_track_set_pivot = centre / static_cast(m_track_set.size()); + m_track_set_pivot = track_set_centre(); m_track_set_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(m_track_set_pivot - frame.camera)); } + auto const rotate{m_track_set_using ? m_track_gizmo_drag.operation == gizmo_operation::rotate : m_gizmo_op == gizmo_operation::rotate}; glm::mat4 delta(1.0f); - frame.manipulate(ImGuizmo::TRANSLATE, m_track_set_gizmo, m_gizmo_snap, &delta); + frame.manipulate(rotate ? ImGuizmo::ROTATE_Y : ImGuizmo::TRANSLATE, m_track_set_gizmo, rotate ? 5.0f : m_gizmo_snap, &delta); if (ImGuizmo::IsUsing()) { - if (false == m_track_set_using) - { - std::string reason; - if (false == tracks_editable(m_track_set, reason)) - { - ui()->set_status(reason, true); - return; - } - m_track_set_using = true; - m_track_drag = m_track_set; - m_track_set_joints.clear(); - for (auto *track : m_track_set) - { - auto joints{editor_track::joints(*track)}; - joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return in_track_set(Joint.other); }), joints.end()); - for (auto const &joint : joints) - if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) - m_track_drag.push_back(joint.other); - m_track_set_joints.emplace_back(track, std::move(joints)); - } - std::vector> states; - for (auto *track : m_track_drag) - states.emplace_back(track, editor_track::capture(*track)); - push_track_snapshot(std::move(states)); - } - glm::dvec3 const offset{delta[3]}; - if (glm::length(offset) > 1e-9) - { - for (auto &entry : m_track_set_joints) - { - editor_track::translate(*entry.first, offset); - editor_track::follow(*entry.first, entry.second); - } - m_track_dirty = true; - } + if (false == m_track_set_using && false == track_set_begin(rotate)) + return; + if (rotate) + track_set_turn(std::atan2(static_cast(m_track_set_gizmo[2][0]), static_cast(m_track_set_gizmo[0][0]))); + else if (glm::dvec3 const offset{delta[3]}; glm::length(offset) > 1e-9) + track_set_move(offset); commit_track_drag(false); } else if (m_track_set_using) - { - commit_track_drag(true); - m_track_set_using = false; - m_track_drag.clear(); - m_track_set_joints.clear(); - straight_refresh(); - } + track_set_end(); } void editor_mode::render_lineside_ui() diff --git a/application/editorprojection.h b/application/editorprojection.h index 75aa20ea..a20d0543 100644 --- a/application/editorprojection.h +++ b/application/editorprojection.h @@ -130,11 +130,11 @@ struct gizmo_frame projection = editor_mode::projection_matrix(io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f); } // moves the gizmo, placed at the point given in world space, with snapping to the step when ctrl is held - bool manipulate(ImGuizmo::OPERATION const Operation, glm::mat4 &Gizmo, float const Snap, glm::mat4 *Delta = nullptr) const + bool manipulate(ImGuizmo::OPERATION const Operation, glm::mat4 &Gizmo, float const Snap, glm::mat4 *Delta = nullptr, ImGuizmo::MODE const Mode = ImGuizmo::WORLD) const { glm::vec3 snapvalue(Snap); float const *snap = Global.ctrlState && Snap > 0.0f ? &snapvalue.x : nullptr; drawn = ImGui::GetFrameCount(); - return ImGuizmo::Manipulate(&view[0][0], &projection[0][0], Operation, ImGuizmo::WORLD, &Gizmo[0][0], Delta != nullptr ? &(*Delta)[0][0] : nullptr, snap); + return ImGuizmo::Manipulate(&view[0][0], &projection[0][0], Operation, Mode, &Gizmo[0][0], Delta != nullptr ? &(*Delta)[0][0] : nullptr, snap); } }; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 418d908f..43c10916 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -665,6 +665,89 @@ void editor_track::follow(TTrack const &Track, std::vector const &Joints) } } +std::vector editor_track::hold_controls(TTrack &Track) +{ + std::vector result; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + auto &path{Track.m_paths[i]}; + auto &control1{path.points[segment_data::point::control1]}; + auto &control2{path.points[segment_data::point::control2]}; + if (control1 != glm::dvec3{} || control2 != glm::dvec3{}) + continue; + auto const chord{path.points[segment_data::point::end] - path.points[segment_data::point::start]}; + if (glm::length(chord) < 1e-6) + continue; + control1 = chord / 3.0; + control2 = -chord / 3.0; + result.push_back(i); + } + return result; +} + +void editor_track::settle_controls(TTrack &Track, std::vector const &Paths) +{ + for (auto const index : Paths) + { + if (index < 0 || index >= static_cast(Track.m_paths.size())) + continue; + auto &path{Track.m_paths[index]}; + auto const chord{path.points[segment_data::point::end] - path.points[segment_data::point::start]}; + auto const length{glm::length(chord)}; + if (length < 1e-6) + continue; + auto const axis{chord / length}; + auto const &control1{path.points[segment_data::point::control1]}; + auto const &control2{path.points[segment_data::point::control2]}; + auto const off = [&](glm::dvec3 const &Control) { return glm::length(Control - axis * glm::dot(Control, axis)); }; + if (glm::dot(control1, axis) <= 0.0 || glm::dot(control2, axis) >= 0.0 || off(control1) > 1e-3 || off(control2) > 1e-3) + continue; + path.points[segment_data::point::control1] = glm::dvec3{}; + path.points[segment_data::point::control2] = glm::dvec3{}; + } +} + +editor_track::point_ref editor_track::end_of(point_ref const &Control) +{ + switch (Control.kind) + { + case point_kind::control1: return {Control.path, point_kind::start}; + case point_kind::control2: return {Control.path, point_kind::end}; + default: return Control; + } +} + +std::vector> editor_track::partners(TTrack &Track, point_ref const &End) +{ + auto const position{point_position(Track, End)}; + auto result{connected_points(Track, position)}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + if (i == End.path) + continue; + for (auto const kind : {point_kind::start, point_kind::end}) + if (glm::distance(Track.m_paths[i].points[point_index(kind)], position) < kSamePoint) + result.emplace_back(&Track, point_ref{i, kind}); + } + return result; +} + +void editor_track::align_controls(TTrack const &Track, point_ref const &Control, std::vector> const &Partners) +{ + auto const own{point_position(Track, Control) - point_position(Track, end_of(Control))}; + if (glm::length(own) < 1e-6) + return; + auto const direction{glm::normalize(own)}; + for (auto const &partner : Partners) + { + auto &path{partner.first->m_paths[partner.second.path]}; + bool const start{partner.second.kind == point_kind::start}; + auto &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; + auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end]) / 3.0}; + control = -direction * length; + } +} + bool editor_track::is_connected(TTrack const &Track, point_ref const &Point) { if (false == editor_track::is_end(Point.kind)) diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 43d42762..272c8d9c 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -116,6 +116,11 @@ class editor_track static std::vector joints(TTrack const &Track); // moves the joined ends to where the ends of the track are now, turning them as the track turned there static void follow(TTrack const &Track, std::vector const &Joints); + static std::vector hold_controls(TTrack &Track); + static void settle_controls(TTrack &Track, std::vector const &Paths); + static std::vector> partners(TTrack &Track, point_ref const &End); + static void align_controls(TTrack const &Track, point_ref const &Control, std::vector> const &Partners); + static point_ref end_of(point_ref const &Control); struct snap_target { diff --git a/lang/editor_pl.po b/lang/editor_pl.po index e95c4750..32505061 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5268,3 +5268,39 @@ msgstr "kładzie prostą dotąd i idzie dalej od jej końca" msgid "type the radius" msgstr "wpisz promień" + +msgid "moves the control point, the joined track keeps the tangent" +msgstr "przesuwa punkt kontrolny, dołączony tor zachowuje styczność" + +msgid "turns the control vector, the joined track keeps the tangent" +msgstr "obraca wektor kontrolny, dołączony tor zachowuje styczność" + +msgid "length of the control vector" +msgstr "długość wektora kontrolnego" + +msgid "turns the track about the point, the joined tracks follow" +msgstr "obraca tor wokół punktu, dołączone tory idą za nim" + +msgid "lengthens the track at this end, the curve keeps its radius" +msgstr "wydłuża tor na tym końcu, łuk zachowuje promień" + +msgid "turns the track, the joined tracks follow" +msgstr "obraca tor, dołączone tory idą za nim" + +msgid "lengthens the track at its free end, the curve keeps its radius" +msgstr "wydłuża tor na wolnym końcu, łuk zachowuje promień" + +msgid "gizmo: move, turn, lengthen" +msgstr "gizmo: przesuń, obróć, wydłuż" + +msgid "control %.2f m azimuth %.2f deg" +msgstr "wektor %.2f m azymut %.2f°" + +msgid " turned %+.2f deg" +msgstr " obrót %+.2f°" + +msgid "turned %+.2f deg" +msgstr "obrót %+.2f°" + +msgid "along the switch" +msgstr "wzdłuż rozjazdu" From b486eb33d59a7c8a4e37726cc20c20d770daf210 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 13:06:45 +0200 Subject: [PATCH 009/150] fix(editor): a track lengthened past 90 degrees goes on as new tracks instead of hidden paths A regular track draws and saves only its first path; the pieces of a lengthened curve past the first are now tracks of their own, made and dropped while dragging, taken back by one undo. --- application/editormode.h | 1 + application/editormodetrack.cpp | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/application/editormode.h b/application/editormode.h index 1f3fc688..1d387c1c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -656,6 +656,7 @@ class editor_mode : public application_mode, private editor_track::observer std::vector> partners; glm::dvec3 fixed{0.0}; std::vector>> held; + std::vector extra; }; track_gizmo_drag m_track_gizmo_drag; gizmo_operation track_gizmo_operation(TTrack const &Track, bool const Pointmode) const; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4b283efe..f1fc28da 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -592,6 +592,39 @@ void editor_mode::track_gizmo_stretch(TTrack &Track, double const Length) for (auto &joint : joints) joint.own = stretch_remap(joint.own, drag.path, drag.pieces); editor_track::follow(Track, joints); + std::vector rest; + if (Track.eType == tt_Normal && pieces.size() > 1) + { + auto const first{Track.m_paths.begin() + drag.path}; + auto const keep{drag.atend ? first : first + (pieces.size() - 1)}; + for (auto piece = first; piece != first + pieces.size(); ++piece) + if (piece != keep) + rest.push_back(*piece); + auto const kept{*keep}; + Track.m_paths.erase(first, first + pieces.size()); + Track.m_paths.insert(Track.m_paths.begin() + drag.path, kept); + if (drag.point.valid()) + m_track_point = drag.point; + } + while (drag.extra.size() > rest.size()) + { + auto *surplus{drag.extra.back()}; + drag.extra.pop_back(); + m_track_drag.erase(std::remove(m_track_drag.begin(), m_track_drag.end(), surplus), m_track_drag.end()); + editor_track::retire(*surplus); + } + for (std::size_t i = 0; i < rest.size(); ++i) + { + if (i < drag.extra.size()) + { + drag.extra[i]->m_paths = {rest[i]}; + continue; + } + auto *created{editor_track::create_path(Track, rest[i])}; + drag.extra.push_back(created); + m_track_drag.push_back(created); + editor_track::commit({created}); + } m_track_dirty = true; } @@ -728,6 +761,12 @@ void editor_mode::track_gizmo_end(TTrack &Track) m_track_dirty = true; m_track_snap = {}; commit_track_drag(true); + if (false == m_track_gizmo_drag.extra.empty() && false == m_history.empty()) + { + auto &created{m_history.back().created}; + created.insert(created.end(), m_track_gizmo_drag.extra.begin(), m_track_gizmo_drag.extra.end()); + m_track_gizmo_drag.extra.clear(); + } m_track_gizmo_using = false; m_track_drag.clear(); m_track_drag_points.clear(); From a756beb34b36d28aa6b12a25c9927e70e18b8014 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 13:22:47 +0200 Subject: [PATCH 010/150] feat(editor): track gizmo rings along the track: direction, grade and cant The rotate gizmo stands along the track. The cross ring changes the grade about the middle or the selected end, the joined tracks take the grade at the joints; the ring along the track changes the cant of the track or of the selected end, the joined tracks take it at the joints and ramp it. On a control vector the cross ring tilts it up or down. Ctrl steps by 5 degrees, 1 per mille, 5 mm. --- application/editormode.h | 11 +- application/editormodeselftest.cpp | 7 +- application/editormodetrack.cpp | 198 ++++++++++++++++++++++++++--- editor/editorTrack.cpp | 13 ++ editor/editorTrack.hpp | 1 + lang/editor_pl.po | 21 ++- 6 files changed, 225 insertions(+), 26 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 1d387c1c..8b8ac018 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -657,6 +657,15 @@ class editor_mode : public application_mode, private editor_track::observer glm::dvec3 fixed{0.0}; std::vector>> held; std::vector extra; + int ring{-1}; + glm::dmat3 basis{1.0}; + glm::dvec3 pivot{0.0}; + double grade{0.0}; + double roll{0.0}; + double roll0{0.0}; + double pivot_chainage{0.0}; + std::vector chainages; + std::vector lengths; }; track_gizmo_drag m_track_gizmo_drag; gizmo_operation track_gizmo_operation(TTrack const &Track, bool const Pointmode) const; @@ -666,7 +675,7 @@ class editor_mode : public application_mode, private editor_track::observer void track_gizmo_stretch(TTrack &Track, double const Length); void track_gizmo_control(TTrack &Track, glm::dvec3 Control); void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); - void track_gizmo_turn(TTrack &Track, double const Angle); + void track_gizmo_turn(TTrack &Track, int const Axis, double const Angle); void track_gizmo_length(TTrack &Track, double Length); void track_gizmo_end(TTrack &Track); bool m_track_dirty{false}; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 9e4e29e5..ed657a7c 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1189,8 +1189,11 @@ void editor_mode::selftest_step() else if (action == "turn") { double degrees{0.0}; - words >> degrees; - track_gizmo_turn(*track, glm::radians(degrees)); + std::string axis{"y"}; + words >> degrees >> axis; + m_track_gizmo_drag.ring = axis == "x" ? 0 : axis == "z" ? 2 : 1; + track_gizmo_turn(*track, m_track_gizmo_drag.ring, glm::radians(degrees)); + WriteLog(format("SELFTEST gizmo turn %s: %s", axis.c_str(), track_readout().c_str())); } else if (action == "length") { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index f1fc28da..299dc50f 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -381,6 +381,42 @@ glm::mat4 gizmo_along(glm::dvec3 const &Position, glm::dvec3 const &Axis) return gizmo; } +void track_middle(TTrack const &Track, glm::dvec3 &Point, glm::dvec3 &Tangent, double &Chainage) +{ + double total{0.0}; + for (auto const &path : Track.m_paths) + total += bezier{path}.plan_length(); + Chainage = total * 0.5; + double along{0.0}; + for (auto const &path : Track.m_paths) + { + bezier const curve{path}; + auto const length{curve.plan_length()}; + if (along + length >= Chainage || &path == &Track.m_paths.back()) + { + auto const t{curve.parameter(Chainage - along)}; + Point = curve.point(t); + Tangent = curve.first(t); + return; + } + along += length; + } + Point = Track.location(); + Tangent = {0.0, 0.0, 1.0}; +} + +glm::dvec3 level(glm::dvec3 const &Vector) +{ + glm::dvec3 const flat{Vector.x, 0.0, Vector.z}; + return glm::length(flat) > 1e-9 ? glm::normalize(flat) : glm::dvec3{0.0, 0.0, 1.0}; +} + +double roll_into(TTrack const &Track, editor_track::point_ref const &Point) +{ + auto const &path{Track.m_paths[Point.path]}; + return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; +} + editor_track::point_ref stretch_remap(editor_track::point_ref Point, int const Path, std::size_t const Pieces) { if (false == Point.valid() || Point.path < Path) @@ -550,6 +586,19 @@ glm::mat4 editor_mode::track_gizmo_frame(TTrack const &Track, bool const Pointmo else if (stretch_end(Track, Pointmode ? m_track_point : editor_track::point_ref{}, path, atend)) std::tie(anchor, axis) = stretch_axis(Track.m_paths[path], atend); } + else if (Operation == gizmo_operation::rotate) + { + if (Pointmode && false == editor_track::is_end(m_track_point.kind)) + axis = level(anchor - editor_track::point_position(Track, editor_track::end_of(m_track_point))); + else if (Pointmode) + axis = level(bezier{Track.m_paths[m_track_point.path]}.first(m_track_point.kind == editor_track::point_kind::start ? 0.0 : 1.0)); + else + { + double chainage{0.0}; + track_middle(Track, anchor, axis, chainage); + axis = level(axis); + } + } return gizmo_along(anchor - glm::dvec3{GfxRenderer->Camera_Position()}, axis); } @@ -717,24 +766,120 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o drag.length = bezier{Track.m_paths[drag.path]}.plan_length(); } drag.stretch = drag.length; + if (Operation == gizmo_operation::rotate) + { + auto const frame{track_gizmo_frame(Track, Pointmode, Operation)}; + drag.basis = glm::dmat3(glm::mat3(frame)); + drag.pivot = glm::dvec3{GfxRenderer->Camera_Position()} + glm::dvec3(frame[3]); + double along{0.0}; + for (auto const &path : Track.m_paths) + { + drag.chainages.push_back(along); + drag.lengths.push_back(bezier{path}.plan_length()); + along += drag.lengths.back(); + } + if (drag.point.valid() && editor_track::is_end(drag.point.kind) && drag.point.path < static_cast(drag.chainages.size())) + { + drag.pivot_chainage = drag.chainages[drag.point.path] + (drag.point.kind == editor_track::point_kind::end ? drag.lengths[drag.point.path] : 0.0); + drag.roll0 = drag.point.kind == editor_track::point_kind::start ? Track.m_paths[drag.point.path].rolls[0] : Track.m_paths[drag.point.path].rolls[1]; + } + else + { + drag.pivot_chainage = along * 0.5; + drag.roll0 = Track.m_paths.empty() ? 0.0 : Track.m_paths.front().rolls[0]; + } + } } -void editor_mode::track_gizmo_turn(TTrack &Track, double const Angle) +void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const Angle) { auto &drag{m_track_gizmo_drag}; - auto const step{std::remainder(Angle - drag.angle, 2.0 * glm::pi())}; - if (std::abs(step) < 1e-9 || (drag.control && drag.fixed != glm::dvec3{})) + if (drag.control && drag.fixed != glm::dvec3{}) return; - drag.angle += step; - if (drag.control) + auto const snap{Global.ctrlState}; + if (Axis == 1) { - auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(drag.point))}; - auto const plan{turned(plan_of(control), -step)}; - track_gizmo_control(Track, {plan.x, control.y, plan.y}); + auto target{Angle}; + if (snap) + target = glm::radians(std::round(glm::degrees(target) / 5.0) * 5.0); + auto const step{std::remainder(target - drag.angle, 2.0 * glm::pi())}; + if (std::abs(step) < 1e-9) + return; + drag.angle += step; + if (drag.control) + { + auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(drag.point))}; + auto const plan{turned(plan_of(control), -step)}; + track_gizmo_control(Track, {plan.x, control.y, plan.y}); + return; + } + editor_track::rotate_y(Track, drag.pivot, step); + editor_track::follow(Track, m_track_joints); + m_track_dirty = true; return; } - editor_track::rotate_y(Track, drag.point.valid() ? editor_track::point_position(Track, drag.point) : m_track_pivot, step); - editor_track::follow(Track, m_track_joints); + if (Axis == 0) + { + auto target{-std::tan(std::clamp(Angle, -1.4, 1.4))}; + if (snap) + target = std::round(target * 1000.0) / 1000.0; + auto const step{target - drag.grade}; + if (std::abs(step) < 1e-9) + return; + drag.grade = target; + if (drag.control) + { + auto const control{editor_track::point_position(Track, drag.point) - editor_track::point_position(Track, editor_track::end_of(drag.point))}; + auto const run{glm::length(plan_of(control))}; + auto const along{glm::dot(level(control), drag.basis[2])}; + track_gizmo_control(Track, {control.x, control.y + step * run * (along >= 0.0 ? 1.0 : -1.0), control.z}); + return; + } + for (std::size_t i = 0; i < Track.m_paths.size() && i < drag.chainages.size(); ++i) + { + auto &path{Track.m_paths[i]}; + auto const from{drag.chainages[i] - drag.pivot_chainage}; + auto const to{from + drag.lengths[i]}; + path.points[segment_data::point::start].y += step * from; + path.points[segment_data::point::end].y += step * to; + auto &control1{path.points[segment_data::point::control1]}; + auto &control2{path.points[segment_data::point::control2]}; + control1.y += step * glm::length(plan_of(control1)); + control2.y -= step * glm::length(plan_of(control2)); + } + editor_track::follow(Track, m_track_joints); + editor_track::follow_grade(Track, m_track_joints); + m_track_dirty = true; + return; + } + if (drag.control) + return; + auto target{glm::degrees(Angle)}; + if (snap) + target = glm::degrees(std::asin(std::clamp(std::round(1500.0 * std::sin(glm::radians(drag.roll0 + target)) / 5.0) * 5.0 / 1500.0, -1.0, 1.0))) - drag.roll0; + auto const step{static_cast(target - drag.roll)}; + if (std::abs(step) < 1e-6f) + return; + drag.roll = target; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + auto &path{Track.m_paths[i]}; + if (false == drag.point.valid() || (i == drag.point.path && drag.point.kind == editor_track::point_kind::start)) + path.rolls[0] += step; + if (false == drag.point.valid() || (i == drag.point.path && drag.point.kind == editor_track::point_kind::end)) + path.rolls[1] += step; + } + for (auto const &joint : m_track_joints) + { + if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) + continue; + auto const roll{static_cast(roll_into(Track, joint.own))}; + auto &theirs{joint.other->m_paths[joint.theirs.path]}; + if (joint.theirs.kind == editor_track::point_kind::start) + theirs.rolls[0] = -roll; + else + theirs.rolls[1] = roll; + } m_track_dirty = true; } @@ -793,9 +938,11 @@ void editor_mode::render_track_gizmo() m_track_gizmo = track_gizmo_frame(*track, pointmode, operation); glm::mat4 delta(1.0f); - auto const gizmo{operation == gizmo_operation::rotate ? ImGuizmo::ROTATE_Y : operation == gizmo_operation::scale ? ImGuizmo::SCALE_Z : ImGuizmo::TRANSLATE}; + auto gizmo{operation == gizmo_operation::scale ? ImGuizmo::SCALE_Z : ImGuizmo::TRANSLATE}; + if (operation == gizmo_operation::rotate) + gizmo = controlmode ? ImGuizmo::ROTATE_X | ImGuizmo::ROTATE_Y : ImGuizmo::ROTATE_X | ImGuizmo::ROTATE_Y | ImGuizmo::ROTATE_Z; ImGuizmo::AllowAxisFlip(operation != gizmo_operation::scale); - frame.manipulate(gizmo, m_track_gizmo, operation == gizmo_operation::rotate ? 5.0f : operation == gizmo_operation::scale ? 0.0f : m_gizmo_snap, &delta, operation == gizmo_operation::scale ? ImGuizmo::LOCAL : ImGuizmo::WORLD); + frame.manipulate(gizmo, m_track_gizmo, operation == gizmo_operation::translate ? m_gizmo_snap : 0.0f, &delta, operation == gizmo_operation::translate ? ImGuizmo::WORLD : ImGuizmo::LOCAL); ImGuizmo::AllowAxisFlip(true); if (ImGuizmo::IsUsing()) @@ -808,7 +955,18 @@ void editor_mode::render_track_gizmo() } if (operation == gizmo_operation::rotate) - track_gizmo_turn(*track, std::atan2(static_cast(m_track_gizmo[2][0]), static_cast(m_track_gizmo[0][0]))); + { + auto const turn{glm::transpose(drag.basis) * glm::dmat3(glm::mat3(m_track_gizmo))}; + std::array const angles{std::atan2(turn[1][2], turn[1][1]), std::atan2(turn[2][0], turn[0][0]), std::atan2(turn[0][1], turn[0][0])}; + if (drag.ring < 0) + { + auto const largest{std::max_element(angles.begin(), angles.end(), [](double const A, double const B) { return std::abs(A) < std::abs(B); })}; + if (std::abs(*largest) > 1e-5) + drag.ring = static_cast(largest - angles.begin()); + } + if (drag.ring >= 0) + track_gizmo_turn(*track, drag.ring, angles[drag.ring]); + } else if (operation == gizmo_operation::scale) { double along{0.0}; @@ -3968,9 +4126,9 @@ std::vector editor_mode::track_key_hints(bool const All) { char const *action{"moves the point along the axes"}; if (false == editor_track::is_end(m_track_point.kind)) - action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent"; + action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector sideways or up, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent"; else if (m_gizmo_op == gizmo_operation::rotate) - action = "turns the track about the point, the joined tracks follow"; + action = "rings about the point: direction, grade, cant; the joined tracks follow"; else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, true) == gizmo_operation::scale) action = "lengthens the track at this end, the curve keeps its radius"; hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Esc", "releases the point"}}; @@ -3980,7 +4138,7 @@ std::vector editor_mode::track_key_hints(bool const All) { char const *action{"moves the whole track"}; if (m_gizmo_op == gizmo_operation::rotate) - action = "turns the track, the joined tracks follow"; + action = "rings: direction, grade, cant; the joined tracks follow"; else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, false) == gizmo_operation::scale) action = "lengthens the track at its free end, the curve keeps its radius"; hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", action}, {"Q W E", "gizmo: move, turn, lengthen"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}}; @@ -4119,8 +4277,10 @@ std::string editor_mode::track_readout() const { auto const control{editor_track::point_position(*track, m_track_point) - editor_track::point_position(*track, editor_track::end_of(m_track_point))}; auto text{format(STR_C("control %.2f m azimuth %.2f deg"), glm::length(control), azimuth(glm::dvec3{0.0}, control))}; - if (drag.operation == gizmo_operation::rotate) + if (drag.operation == gizmo_operation::rotate && drag.ring == 1) text += format(STR_C(" turned %+.2f deg"), glm::degrees(drag.angle)); + if (drag.operation == gizmo_operation::rotate && drag.ring == 0) + text += format(STR_C(" grade %+.1f per mille"), 1000.0 * grade_of(control) * (glm::dot(level(control), drag.basis[2]) >= 0.0 ? 1.0 : -1.0)); if (drag.fixed != glm::dvec3{}) text += "\n" + STR("along the switch"); return text; @@ -4132,6 +4292,10 @@ std::string editor_mode::track_readout() const text += format(" R %.1f m", std::abs(drag.paths[drag.path].radius)); return text; } + if (drag.operation == gizmo_operation::rotate && drag.ring == 0) + return format(STR_C("grade changed by %+.1f per mille"), 1000.0 * drag.grade); + if (drag.operation == gizmo_operation::rotate && drag.ring == 2) + return format(STR_C("cant %.0f mm (%+.0f mm)"), 1500.0 * std::sin(glm::radians(drag.roll0 + drag.roll)), 1500.0 * (std::sin(glm::radians(drag.roll0 + drag.roll)) - std::sin(glm::radians(drag.roll0)))); if (drag.operation == gizmo_operation::rotate) return format(STR_C("turned %+.2f deg"), glm::degrees(drag.angle)); if (m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size())) diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 43c10916..54466bd0 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -665,6 +665,19 @@ void editor_track::follow(TTrack const &Track, std::vector const &Joints) } } +void editor_track::follow_grade(TTrack const &Track, std::vector const &Joints) +{ + for (auto const &joint : Joints) + { + if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) + continue; + geometry::bezier const own{Track.m_paths[joint.own.path]}; + auto const outward{joint.own.kind == point_kind::start ? -own.first(0.0) : own.first(1.0)}; + auto &control{joint.other->m_paths[joint.theirs.path].points[joint.theirs.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]}; + control.y = geometry::grade_of(outward) * glm::length(geometry::plan_of(control)); + } +} + std::vector editor_track::hold_controls(TTrack &Track) { std::vector result; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 272c8d9c..f4ee9ee8 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -116,6 +116,7 @@ class editor_track static std::vector joints(TTrack const &Track); // moves the joined ends to where the ends of the track are now, turning them as the track turned there static void follow(TTrack const &Track, std::vector const &Joints); + static void follow_grade(TTrack const &Track, std::vector const &Joints); static std::vector hold_controls(TTrack &Track); static void settle_controls(TTrack &Track, std::vector const &Paths); static std::vector> partners(TTrack &Track, point_ref const &End); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 32505061..fd2e1b35 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5272,20 +5272,20 @@ msgstr "wpisz promień" msgid "moves the control point, the joined track keeps the tangent" msgstr "przesuwa punkt kontrolny, dołączony tor zachowuje styczność" -msgid "turns the control vector, the joined track keeps the tangent" -msgstr "obraca wektor kontrolny, dołączony tor zachowuje styczność" +msgid "turns the control vector sideways or up, the joined track keeps the tangent" +msgstr "obraca wektor kontrolny w bok lub w górę, dołączony tor zachowuje styczność" msgid "length of the control vector" msgstr "długość wektora kontrolnego" -msgid "turns the track about the point, the joined tracks follow" -msgstr "obraca tor wokół punktu, dołączone tory idą za nim" +msgid "rings about the point: direction, grade, cant; the joined tracks follow" +msgstr "pierścienie wokół punktu: kierunek, pochylenie, przechyłka; dołączone tory idą za nim" msgid "lengthens the track at this end, the curve keeps its radius" msgstr "wydłuża tor na tym końcu, łuk zachowuje promień" -msgid "turns the track, the joined tracks follow" -msgstr "obraca tor, dołączone tory idą za nim" +msgid "rings: direction, grade, cant; the joined tracks follow" +msgstr "pierścienie: kierunek, pochylenie, przechyłka; dołączone tory idą za nim" msgid "lengthens the track at its free end, the curve keeps its radius" msgstr "wydłuża tor na wolnym końcu, łuk zachowuje promień" @@ -5304,3 +5304,12 @@ msgstr "obrót %+.2f°" msgid "along the switch" msgstr "wzdłuż rozjazdu" + +msgid " grade %+.1f per mille" +msgstr " pochylenie %+.1f‰" + +msgid "grade changed by %+.1f per mille" +msgstr "pochylenie zmienione o %+.1f‰" + +msgid "cant %.0f mm (%+.0f mm)" +msgstr "przechyłka %.0f mm (%+.0f mm)" From 34941822fffade2fde79e227584e24d63a3043be Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 13:40:29 +0200 Subject: [PATCH 011/150] fix(editor): the cant ring works on a control vector, it cants the end of the vector and the tracks joined there --- application/editormodetrack.cpp | 31 +++++++++++++++++++++---------- lang/editor_pl.po | 4 ++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 299dc50f..1e39454e 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -778,10 +778,11 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o drag.lengths.push_back(bezier{path}.plan_length()); along += drag.lengths.back(); } - if (drag.point.valid() && editor_track::is_end(drag.point.kind) && drag.point.path < static_cast(drag.chainages.size())) + auto const end{editor_track::end_of(drag.point)}; + if (end.valid() && end.path < static_cast(drag.chainages.size())) { - drag.pivot_chainage = drag.chainages[drag.point.path] + (drag.point.kind == editor_track::point_kind::end ? drag.lengths[drag.point.path] : 0.0); - drag.roll0 = drag.point.kind == editor_track::point_kind::start ? Track.m_paths[drag.point.path].rolls[0] : Track.m_paths[drag.point.path].rolls[1]; + drag.pivot_chainage = drag.chainages[end.path] + (end.kind == editor_track::point_kind::end ? drag.lengths[end.path] : 0.0); + drag.roll0 = end.kind == editor_track::point_kind::start ? Track.m_paths[end.path].rolls[0] : Track.m_paths[end.path].rolls[1]; } else { @@ -852,9 +853,8 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A m_track_dirty = true; return; } - if (drag.control) - return; - auto target{glm::degrees(Angle)}; + auto const end{editor_track::end_of(drag.point)}; + auto target{glm::degrees(drag.point.kind == editor_track::point_kind::control2 ? -Angle : Angle)}; if (snap) target = glm::degrees(std::asin(std::clamp(std::round(1500.0 * std::sin(glm::radians(drag.roll0 + target)) / 5.0) * 5.0 / 1500.0, -1.0, 1.0))) - drag.roll0; auto const step{static_cast(target - drag.roll)}; @@ -864,9 +864,9 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) { auto &path{Track.m_paths[i]}; - if (false == drag.point.valid() || (i == drag.point.path && drag.point.kind == editor_track::point_kind::start)) + if (false == end.valid() || (i == end.path && end.kind == editor_track::point_kind::start)) path.rolls[0] += step; - if (false == drag.point.valid() || (i == drag.point.path && drag.point.kind == editor_track::point_kind::end)) + if (false == end.valid() || (i == end.path && end.kind == editor_track::point_kind::end)) path.rolls[1] += step; } for (auto const &joint : m_track_joints) @@ -880,6 +880,15 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A else theirs.rolls[1] = roll; } + for (auto const &partner : drag.partners) + { + auto const roll{static_cast(roll_into(Track, end))}; + auto &theirs{partner.first->m_paths[partner.second.path]}; + if (partner.second.kind == editor_track::point_kind::start) + theirs.rolls[0] = -roll; + else + theirs.rolls[1] = roll; + } m_track_dirty = true; } @@ -940,7 +949,7 @@ void editor_mode::render_track_gizmo() glm::mat4 delta(1.0f); auto gizmo{operation == gizmo_operation::scale ? ImGuizmo::SCALE_Z : ImGuizmo::TRANSLATE}; if (operation == gizmo_operation::rotate) - gizmo = controlmode ? ImGuizmo::ROTATE_X | ImGuizmo::ROTATE_Y : ImGuizmo::ROTATE_X | ImGuizmo::ROTATE_Y | ImGuizmo::ROTATE_Z; + gizmo = ImGuizmo::ROTATE_X | ImGuizmo::ROTATE_Y | ImGuizmo::ROTATE_Z; ImGuizmo::AllowAxisFlip(operation != gizmo_operation::scale); frame.manipulate(gizmo, m_track_gizmo, operation == gizmo_operation::translate ? m_gizmo_snap : 0.0f, &delta, operation == gizmo_operation::translate ? ImGuizmo::WORLD : ImGuizmo::LOCAL); ImGuizmo::AllowAxisFlip(true); @@ -4126,7 +4135,7 @@ std::vector editor_mode::track_key_hints(bool const All) { char const *action{"moves the point along the axes"}; if (false == editor_track::is_end(m_track_point.kind)) - action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector sideways or up, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent"; + action = m_gizmo_op == gizmo_operation::rotate ? "turns the control vector sideways or up, cant at its end, the joined track keeps the tangent" : m_gizmo_op == gizmo_operation::scale ? "length of the control vector" : "moves the control point, the joined track keeps the tangent"; else if (m_gizmo_op == gizmo_operation::rotate) action = "rings about the point: direction, grade, cant; the joined tracks follow"; else if (m_gizmo_op == gizmo_operation::scale && selected != nullptr && track_gizmo_operation(*selected, true) == gizmo_operation::scale) @@ -4281,6 +4290,8 @@ std::string editor_mode::track_readout() const text += format(STR_C(" turned %+.2f deg"), glm::degrees(drag.angle)); if (drag.operation == gizmo_operation::rotate && drag.ring == 0) text += format(STR_C(" grade %+.1f per mille"), 1000.0 * grade_of(control) * (glm::dot(level(control), drag.basis[2]) >= 0.0 ? 1.0 : -1.0)); + if (drag.operation == gizmo_operation::rotate && drag.ring == 2) + text += " " + format(STR_C("cant %.0f mm (%+.0f mm)"), 1500.0 * std::sin(glm::radians(drag.roll0 + drag.roll)), 1500.0 * (std::sin(glm::radians(drag.roll0 + drag.roll)) - std::sin(glm::radians(drag.roll0)))); if (drag.fixed != glm::dvec3{}) text += "\n" + STR("along the switch"); return text; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index fd2e1b35..caade69d 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5272,8 +5272,8 @@ msgstr "wpisz promień" msgid "moves the control point, the joined track keeps the tangent" msgstr "przesuwa punkt kontrolny, dołączony tor zachowuje styczność" -msgid "turns the control vector sideways or up, the joined track keeps the tangent" -msgstr "obraca wektor kontrolny w bok lub w górę, dołączony tor zachowuje styczność" +msgid "turns the control vector sideways or up, cant at its end, the joined track keeps the tangent" +msgstr "obraca wektor kontrolny w bok lub w górę, przechyłka na jego końcu, dołączony tor zachowuje styczność" msgid "length of the control vector" msgstr "długość wektora kontrolnego" From c722e4e419c4eaa38561f4119912bcf4c9a6dcec Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 14:04:05 +0200 Subject: [PATCH 012/150] fix(editor): the gizmo keeps the diverging path of a switch on the surface of its main path The diverging path p3-p4 takes its heights, grades at the ends and cant from the main path p1-p2 (the cone of its cant) after every gizmo change: grade and cant rings, moving an end and editing a control vector. The tracks joined to it follow its ends and grades. The grade ring measures along the main path and the gizmo stands in its middle; the start vector of the diverging path slides along the main one and turns with it. --- application/editormode.h | 1 + application/editormodeselftest.cpp | 9 +-- application/editormodetrack.cpp | 86 ++++++++++++++++---- editor/editorTrack.cpp | 126 ++++++++++++++++++++++++++++- editor/editorTrack.hpp | 2 + 5 files changed, 203 insertions(+), 21 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 8b8ac018..f6453368 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -674,6 +674,7 @@ class editor_mode : public application_mode, private editor_track::observer void track_gizmo_settle(); void track_gizmo_stretch(TTrack &Track, double const Length); void track_gizmo_control(TTrack &Track, glm::dvec3 Control); + void track_gizmo_point(TTrack &Track, glm::dvec3 const &Position); void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); void track_gizmo_turn(TTrack &Track, int const Axis, double const Angle); void track_gizmo_length(TTrack &Track, double Length); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index ed657a7c..67f1a78c 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -357,7 +357,9 @@ void editor_mode::selftest_step() auto const lateral{(point.x - base.x) * -direction.y + (point.z - base.z) * direction.x}; auto const &path{track->m_paths[0]}; auto const roll{glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * along)}; - auto const cone{base.y - lateral * std::tan(roll)}; + auto const tangent{main.first(along)}; + auto const beyond{(point.x - base.x) * direction.x + (point.z - base.z) * direction.y}; + auto const cone{base.y + beyond * tangent.y / glm::length(geometry::plan_of(tangent)) - lateral * std::tan(roll)}; text += format(" [t %.3f off %.3f dy %.4f]", t, lateral, point.y - cone); } WriteLog("SELFTEST conecheck " + name + ":" + text); @@ -1214,10 +1216,7 @@ void editor_mode::selftest_step() words >> offset.x >> offset.y >> offset.z; if (m_track_point.valid()) { - auto const position{editor_track::point_position(*track, m_track_point) + offset}; - editor_track::move_point(*track, m_track_point, position); - for (auto const &connection : m_track_drag_points) - editor_track::move_point(*connection.first, connection.second, position); + track_gizmo_point(*track, editor_track::point_position(*track, m_track_point) + offset); } else { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1e39454e..2a978ddd 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -383,16 +383,18 @@ glm::mat4 gizmo_along(glm::dvec3 const &Position, glm::dvec3 const &Axis) void track_middle(TTrack const &Track, glm::dvec3 &Point, glm::dvec3 &Tangent, double &Chainage) { + auto const count{Track.eType == tt_Switch ? std::min(1, Track.m_paths.size()) : Track.m_paths.size()}; double total{0.0}; - for (auto const &path : Track.m_paths) - total += bezier{path}.plan_length(); + for (std::size_t i = 0; i < count; ++i) + total += bezier{Track.m_paths[i]}.plan_length(); Chainage = total * 0.5; double along{0.0}; - for (auto const &path : Track.m_paths) + for (std::size_t i = 0; i < count; ++i) { + auto const &path{Track.m_paths[i]}; bezier const curve{path}; auto const length{curve.plan_length()}; - if (along + length >= Chainage || &path == &Track.m_paths.back()) + if (along + length >= Chainage || i + 1 == count) { auto const t{curve.parameter(Chainage - along)}; Point = curve.point(t); @@ -417,6 +419,13 @@ double roll_into(TTrack const &Track, editor_track::point_ref const &Point) return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; } +editor_track::point_ref switch_main(TTrack const &Track, editor_track::point_ref Point) +{ + if (Track.eType == tt_Switch && Point.valid() && Point.path > 0) + Point.path = 0; + return Point; +} + editor_track::point_ref stretch_remap(editor_track::point_ref Point, int const Path, std::size_t const Pieces) { if (false == Point.valid() || Point.path < Path) @@ -686,9 +695,29 @@ void editor_mode::track_gizmo_control(TTrack &Track, glm::dvec3 Control) Control = glm::length(Control) > 1e-9 ? glm::normalize(Control) * 0.5 : drag.axis * 0.5; editor_track::move_point(Track, drag.point, editor_track::point_position(Track, editor_track::end_of(drag.point)) + Control); editor_track::align_controls(Track, drag.point, drag.partners); + if (Track.eType == tt_Switch) + { + editor_track::conform_switch(Track); + editor_track::follow(Track, m_track_joints); + editor_track::follow_grade(Track, m_track_joints); + } m_track_dirty = true; } +void editor_mode::track_gizmo_point(TTrack &Track, glm::dvec3 const &Position) +{ + editor_track::move_point(Track, m_track_point, Position); + if (Track.eType == tt_Switch) + { + editor_track::conform_switch(Track); + editor_track::follow(Track, m_track_joints); + editor_track::follow_grade(Track, m_track_joints); + } + auto const moved{editor_track::point_position(Track, m_track_point)}; + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, moved); +} + void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation) { bool const controlmode{Pointmode && false == editor_track::is_end(m_track_point.kind)}; @@ -729,14 +758,15 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o { std::string reason; auto const &path{partner.first->m_paths[partner.second.path]}; - if (partner.first->eType != tt_Normal || false == editor_track::can_edit_geometry(*partner.first, reason)) + bool const own{partner.first == &Track}; + if (own ? m_track_point.path != 0 : partner.first->eType != tt_Normal || false == editor_track::can_edit_geometry(*partner.first, reason)) { bool const start{partner.second.kind == editor_track::point_kind::start}; auto const theirs{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; auto const chord{path.points[start ? segment_data::point::end : segment_data::point::start] - path.points[start ? segment_data::point::start : segment_data::point::end]}; auto const along{theirs != glm::dvec3{} ? theirs : chord}; if (glm::length(along) > 1e-9) - drag.fixed = -glm::normalize(along); + drag.fixed = (own && partner.second.kind == editor_track::end_of(m_track_point).kind ? 1.0 : -1.0) * glm::normalize(along); continue; } if (false == m_track_drag_connected && partner.first != &Track) @@ -746,6 +776,18 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o m_track_drag.emplace_back(partner.first); } } + if (false == whole && Track.eType == tt_Switch && m_track_drag_connected) + { + auto const edited{editor_track::point_position(Track, editor_track::end_of(m_track_point))}; + for (auto const &joint : editor_track::joints(Track)) + { + if (controlmode && glm::distance(editor_track::point_position(Track, joint.own), edited) < geometry::same_point) + continue; + m_track_joints.push_back(joint); + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.emplace_back(joint.other); + } + } std::vector> states; for (auto *dragged : m_track_drag) states.emplace_back(dragged, editor_track::capture(*dragged)); @@ -778,8 +820,13 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o drag.lengths.push_back(bezier{path}.plan_length()); along += drag.lengths.back(); } - auto const end{editor_track::end_of(drag.point)}; - if (end.valid() && end.path < static_cast(drag.chainages.size())) + auto const end{switch_main(Track, editor_track::end_of(drag.point))}; + if (Track.eType == tt_Switch && false == Track.m_paths.empty()) + { + drag.pivot_chainage = editor_track::switch_station(Track, drag.pivot); + drag.roll0 = end.valid() && end.kind == editor_track::point_kind::end ? Track.m_paths[0].rolls[1] : Track.m_paths[0].rolls[0]; + } + else if (end.valid() && end.path < static_cast(drag.chainages.size())) { drag.pivot_chainage = drag.chainages[end.path] + (end.kind == editor_track::point_kind::end ? drag.lengths[end.path] : 0.0); drag.roll0 = end.kind == editor_track::point_kind::start ? Track.m_paths[end.path].rolls[0] : Track.m_paths[end.path].rolls[1]; @@ -836,7 +883,8 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A track_gizmo_control(Track, {control.x, control.y + step * run * (along >= 0.0 ? 1.0 : -1.0), control.z}); return; } - for (std::size_t i = 0; i < Track.m_paths.size() && i < drag.chainages.size(); ++i) + auto const count{Track.eType == tt_Switch ? std::min(1, Track.m_paths.size()) : Track.m_paths.size()}; + for (std::size_t i = 0; i < count && i < drag.chainages.size(); ++i) { auto &path{Track.m_paths[i]}; auto const from{drag.chainages[i] - drag.pivot_chainage}; @@ -848,12 +896,14 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A control1.y += step * glm::length(plan_of(control1)); control2.y -= step * glm::length(plan_of(control2)); } + editor_track::conform_switch(Track); editor_track::follow(Track, m_track_joints); editor_track::follow_grade(Track, m_track_joints); m_track_dirty = true; return; } auto const end{editor_track::end_of(drag.point)}; + auto const canted{switch_main(Track, end)}; auto target{glm::degrees(drag.point.kind == editor_track::point_kind::control2 ? -Angle : Angle)}; if (snap) target = glm::degrees(std::asin(std::clamp(std::round(1500.0 * std::sin(glm::radians(drag.roll0 + target)) / 5.0) * 5.0 / 1500.0, -1.0, 1.0))) - drag.roll0; @@ -864,11 +914,17 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) { auto &path{Track.m_paths[i]}; - if (false == end.valid() || (i == end.path && end.kind == editor_track::point_kind::start)) + if (false == canted.valid() || (i == canted.path && canted.kind == editor_track::point_kind::start)) path.rolls[0] += step; - if (false == end.valid() || (i == end.path && end.kind == editor_track::point_kind::end)) + if (false == canted.valid() || (i == canted.path && canted.kind == editor_track::point_kind::end)) path.rolls[1] += step; } + if (Track.eType == tt_Switch) + { + editor_track::conform_switch(Track); + editor_track::follow(Track, m_track_joints); + editor_track::follow_grade(Track, m_track_joints); + } for (auto const &joint : m_track_joints) { if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) @@ -882,6 +938,8 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A } for (auto const &partner : drag.partners) { + if (partner.first == &Track) + continue; auto const roll{static_cast(roll_into(Track, end))}; auto &theirs{partner.first->m_paths[partner.second.path]}; if (partner.second.kind == editor_track::point_kind::start) @@ -994,10 +1052,8 @@ void editor_mode::render_track_gizmo() } else if (pointmode) { - glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); - editor_track::move_point(*track, m_track_point, position); - for (auto const &connection : m_track_drag_points) - editor_track::move_point(*connection.first, connection.second, position); + track_gizmo_point(*track, frame.camera + glm::dvec3(m_track_gizmo[3])); + auto const position{editor_track::point_position(*track, m_track_point)}; m_track_dirty = true; if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) { diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 54466bd0..99d3236e 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -678,6 +678,129 @@ void editor_track::follow_grade(TTrack const &Track, std::vector const &J } } +namespace +{ + +struct surface_foot +{ + double t{0.0}; + double along{0.0}; + double lateral{0.0}; + glm::dvec3 base{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; +}; + +surface_foot foot_on(segment_data const &Path, glm::dvec3 const &Position) +{ + bezier const curve{Path}; + auto const distance = [&](double const T) { return glm::distance(plan_of(curve.point(T)), plan_of(Position)); }; + int constexpr samples{200}; + double best{std::numeric_limits::max()}; + int nearest{0}; + for (int i = 0; i <= samples; ++i) + if (auto const d{distance(static_cast(i) / samples)}; d < best) + { + best = d; + nearest = i; + } + auto low{std::max(0, nearest - 1) / static_cast(samples)}; + auto high{std::min(samples, nearest + 1) / static_cast(samples)}; + for (int i = 0; i < 40; ++i) + { + auto const a{low + (high - low) / 3.0}; + auto const b{high - (high - low) / 3.0}; + if (distance(a) < distance(b)) + high = b; + else + low = a; + } + surface_foot foot; + foot.t = std::clamp((low + high) * 0.5, 0.0, 1.0); + foot.base = curve.point(foot.t); + auto const tangent{curve.first(foot.t)}; + auto const plan{plan_of(tangent)}; + auto const run{glm::length(plan)}; + if (run > 1e-9) + { + foot.direction = plan / run; + foot.grade = tangent.y / run; + } + auto const offset{plan_of(Position) - plan_of(foot.base)}; + foot.along = glm::dot(offset, foot.direction); + foot.lateral = glm::dot(offset, glm::dvec2{-foot.direction.y, foot.direction.x}); + return foot; +} + +double foot_roll(segment_data const &Path, surface_foot const &Foot) +{ + return Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * Foot.t; +} + +double surface_height(segment_data const &Path, surface_foot const &Foot) +{ + return Foot.base.y + Foot.along * Foot.grade - Foot.lateral * std::tan(glm::radians(foot_roll(Path, Foot))); +} + +} // namespace + +void editor_track::conform_switch(TTrack &Track) +{ + if (Track.eType != tt_Switch || Track.m_paths.size() != 2) + return; + auto const &main{Track.m_paths[0]}; + auto &path{Track.m_paths[1]}; + auto &start{path.points[segment_data::point::start]}; + auto &end{path.points[segment_data::point::end]}; + auto &control1{path.points[segment_data::point::control1]}; + auto &control2{path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + auto const height = [&](glm::dvec3 const &Point) { return surface_height(main, foot_on(main, Point)); }; + auto const slope = [&](glm::dvec3 const &Point, glm::dvec3 const &Control) { + auto const plan{plan_of(Control)}; + auto const run{glm::length(plan)}; + if (run < 1e-9) + return 0.0; + double constexpr step{0.05}; + glm::dvec3 const offset{plan.x / run * step, 0.0, plan.y / run * step}; + return (4.0 * height(Point + offset) - 3.0 * height(Point) - height(Point + 2.0 * offset)) / (2.0 * step) * run; + }; + start.y = height(start); + end.y = height(end); + control1.y = slope(start, control1); + control2.y = slope(end, control2); + bezier const curve{path}; + for (int i = 0; i < 2; ++i) + { + auto const foot{foot_on(main, i == 0 ? start : end)}; + auto const along{glm::dot(plan_of(curve.first(i)), foot.direction) >= 0.0 ? 1.0 : -1.0}; + path.rolls[i] = static_cast(foot_roll(main, foot) * along); + } +} + +double editor_track::switch_station(TTrack const &Track, glm::dvec3 const &Position) +{ + if (Track.m_paths.empty()) + return 0.0; + auto const &main{Track.m_paths[0]}; + auto const foot{foot_on(main, Position)}; + bezier const curve{main}; + int constexpr steps{64}; + double length{0.0}; + auto previous{plan_of(curve.point(0.0))}; + for (int i = 1; i <= steps; ++i) + { + auto const next{plan_of(curve.point(foot.t * i / steps))}; + length += glm::distance(previous, next); + previous = next; + } + return length + foot.along; +} + std::vector editor_track::hold_controls(TTrack &Track) { std::vector result; @@ -757,7 +880,8 @@ void editor_track::align_controls(TTrack const &Track, point_ref const &Control, bool const start{partner.second.kind == point_kind::start}; auto &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end]) / 3.0}; - control = -direction * length; + auto const same{partner.first == &Track && partner.second.kind == end_of(Control).kind}; + control = (same ? direction : -direction) * length; } } diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index f4ee9ee8..8f8040aa 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -117,6 +117,8 @@ class editor_track // moves the joined ends to where the ends of the track are now, turning them as the track turned there static void follow(TTrack const &Track, std::vector const &Joints); static void follow_grade(TTrack const &Track, std::vector const &Joints); + static void conform_switch(TTrack &Track); + static double switch_station(TTrack const &Track, glm::dvec3 const &Position); static std::vector hold_controls(TTrack &Track); static void settle_controls(TTrack &Track, std::vector const &Paths); static std::vector> partners(TTrack &Track, point_ref const &End); From fe9d78b1729d7f0bfd13e607adea0b8c83c9630e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 14:08:47 +0200 Subject: [PATCH 013/150] fix(editor): a switch takes the cant set by the gizmo on a track joined to it Canting a track at its joint with a switch cants the whole switch, its diverging path goes onto the new cone, and the tracks joined at the other ends of the switch follow it and take the cant at their joints. --- application/editormode.h | 9 +++++ application/editormodetrack.cpp | 66 +++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index f6453368..22e65119 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -666,6 +666,15 @@ class editor_mode : public application_mode, private editor_track::observer double pivot_chainage{0.0}; std::vector chainages; std::vector lengths; + struct switch_joint + { + TTrack *track{nullptr}; + editor_track::point_ref own; + editor_track::point_ref theirs; + double roll{0.0}; + std::vector joints; + }; + std::vector switches; }; track_gizmo_drag m_track_gizmo_drag; gizmo_operation track_gizmo_operation(TTrack const &Track, bool const Pointmode) const; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 2a978ddd..850c2912 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -419,6 +419,21 @@ double roll_into(TTrack const &Track, editor_track::point_ref const &Point) return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; } +void match_rolls(TTrack const &Track, std::vector const &Joints) +{ + for (auto const &joint : Joints) + { + if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) + continue; + auto const roll{static_cast(roll_into(Track, joint.own))}; + auto &theirs{joint.other->m_paths[joint.theirs.path]}; + if (joint.theirs.kind == editor_track::point_kind::start) + theirs.rolls[0] = -roll; + else + theirs.rolls[1] = roll; + } +} + editor_track::point_ref switch_main(TTrack const &Track, editor_track::point_ref Point) { if (Track.eType == tt_Switch && Point.valid() && Point.path > 0) @@ -788,6 +803,31 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o m_track_drag.emplace_back(joint.other); } } + if (Operation == gizmo_operation::rotate && m_track_drag_connected) + { + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + editor_track::point_ref const own{i, kind}; + for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, own))) + { + auto *other{connection.first}; + std::string reason; + if (other->eType != tt_Switch || other->m_paths.size() != 2 || false == editor_track::can_edit_geometry(*other, reason)) + continue; + if (std::any_of(drag.switches.begin(), drag.switches.end(), [&](auto const &Link) { return Link.track == other; })) + continue; + auto joints{editor_track::joints(*other)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return Joint.other == &Track; }), joints.end()); + if (std::find(m_track_drag.begin(), m_track_drag.end(), other) == m_track_drag.end()) + m_track_drag.emplace_back(other); + for (auto const &joint : joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.emplace_back(joint.other); + drag.switches.push_back({other, own, connection.second, roll_into(Track, own), std::move(joints)}); + } + } + } std::vector> states; for (auto *dragged : m_track_drag) states.emplace_back(dragged, editor_track::capture(*dragged)); @@ -925,16 +965,26 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A editor_track::follow(Track, m_track_joints); editor_track::follow_grade(Track, m_track_joints); } - for (auto const &joint : m_track_joints) + match_rolls(Track, m_track_joints); + for (auto const &link : drag.switches) { - if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) + auto const roll{roll_into(Track, link.own)}; + if (std::abs(roll - link.roll) < 1e-6) continue; - auto const roll{static_cast(roll_into(Track, joint.own))}; - auto &theirs{joint.other->m_paths[joint.theirs.path]}; - if (joint.theirs.kind == editor_track::point_kind::start) - theirs.rolls[0] = -roll; - else - theirs.rolls[1] = roll; + auto &main{link.track->m_paths[0]}; + bool const start{link.theirs.kind == editor_track::point_kind::start}; + auto value{start ? -roll : roll}; + if (link.theirs.path != 0) + { + auto const t{start ? 0.0 : 1.0}; + if (glm::dot(plan_of(bezier{link.track->m_paths[link.theirs.path]}.first(t)), plan_of(bezier{main}.first(t))) < 0.0) + value = -value; + } + main.rolls = {static_cast(value), static_cast(value)}; + editor_track::conform_switch(*link.track); + editor_track::follow(*link.track, link.joints); + editor_track::follow_grade(*link.track, link.joints); + match_rolls(*link.track, link.joints); } for (auto const &partner : drag.partners) { From f0ccd375a09d644bd1ccd128a0fef3f5a883959e Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 14:19:11 +0200 Subject: [PATCH 014/150] feat(editor): a switch joined to a track goes along with it as a whole when the gizmo moves, turns or tilts the track The switch, with the switches joined to it, keeps its shape and one grade over its whole length: it is moved, turned and tilted to the end of the track, and the tracks at its other ends follow it. --- application/editormode.h | 7 +- application/editormodeselftest.cpp | 4 + application/editormodetrack.cpp | 137 +++++++++++++++++++++-------- 3 files changed, 112 insertions(+), 36 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 22e65119..2c6d65e9 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -671,8 +671,12 @@ class editor_mode : public application_mode, private editor_track::observer TTrack *track{nullptr}; editor_track::point_ref own; editor_track::point_ref theirs; + glm::dvec3 point{0.0}; + glm::dvec3 tangent{0.0}; double roll{0.0}; - std::vector joints; + bool moved{false}; + std::vector>> cluster; + std::vector>> joints; }; std::vector switches; }; @@ -684,6 +688,7 @@ class editor_mode : public application_mode, private editor_track::observer void track_gizmo_stretch(TTrack &Track, double const Length); void track_gizmo_control(TTrack &Track, glm::dvec3 Control); void track_gizmo_point(TTrack &Track, glm::dvec3 const &Position); + void track_gizmo_switches(TTrack &Track); void track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation); void track_gizmo_turn(TTrack &Track, int const Axis, double const Angle); void track_gizmo_length(TTrack &Track, double Length); diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 67f1a78c..07929123 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1195,6 +1195,7 @@ void editor_mode::selftest_step() words >> degrees >> axis; m_track_gizmo_drag.ring = axis == "x" ? 0 : axis == "z" ? 2 : 1; track_gizmo_turn(*track, m_track_gizmo_drag.ring, glm::radians(degrees)); + track_gizmo_switches(*track); WriteLog(format("SELFTEST gizmo turn %s: %s", axis.c_str(), track_readout().c_str())); } else if (action == "length") @@ -1202,6 +1203,7 @@ void editor_mode::selftest_step() double length{0.0}; words >> length; track_gizmo_length(*track, length); + track_gizmo_switches(*track); WriteLog(format("SELFTEST gizmo length %.3f of %.3f, %zu pieces", m_track_gizmo_drag.stretch, m_track_gizmo_drag.length, m_track_gizmo_drag.pieces)); } else if (action == "control") @@ -1209,6 +1211,7 @@ void editor_mode::selftest_step() glm::dvec3 control{0.0}; words >> control.x >> control.y >> control.z; track_gizmo_control(*track, control); + track_gizmo_switches(*track); } else if (action == "offset") { @@ -1223,6 +1226,7 @@ void editor_mode::selftest_step() editor_track::translate(*track, offset); editor_track::follow(*track, m_track_joints); } + track_gizmo_switches(*track); m_track_dirty = true; } else if (action == "end") diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 850c2912..9cf99544 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -434,6 +434,12 @@ void match_rolls(TTrack const &Track, std::vector const &Jo } } +glm::dvec3 track_outward(TTrack const &Track, editor_track::point_ref const &Point) +{ + bezier const curve{Track.m_paths[Point.path]}; + return Point.kind == editor_track::point_kind::start ? -curve.first(0.0) : curve.first(1.0); +} + editor_track::point_ref switch_main(TTrack const &Track, editor_track::point_ref Point) { if (Track.eType == tt_Switch && Point.valid() && Point.path > 0) @@ -729,8 +735,73 @@ void editor_mode::track_gizmo_point(TTrack &Track, glm::dvec3 const &Position) editor_track::follow_grade(Track, m_track_joints); } auto const moved{editor_track::point_position(Track, m_track_point)}; + auto const &links{m_track_gizmo_drag.switches}; for (auto const &connection : m_track_drag_points) - editor_track::move_point(*connection.first, connection.second, moved); + if (std::none_of(links.begin(), links.end(), [&](auto const &Link) { return std::any_of(Link.cluster.begin(), Link.cluster.end(), [&](auto const &Member) { return Member.first == connection.first; }); })) + editor_track::move_point(*connection.first, connection.second, moved); +} + +void editor_mode::track_gizmo_switches(TTrack &Track) +{ + for (auto &link : m_track_gizmo_drag.switches) + { + if (link.own.path >= static_cast(Track.m_paths.size())) + continue; + auto const point{editor_track::point_position(Track, link.own)}; + auto const tangent{track_outward(Track, link.own)}; + auto const roll{roll_into(Track, link.own)}; + auto const from{level(link.tangent)}; + auto const to{level(tangent)}; + auto const grade{geometry::grade_of(tangent) - geometry::grade_of(link.tangent)}; + bool const changed{glm::distance(point, link.point) > 1e-9 || glm::distance(from, to) > 1e-12 || std::abs(grade) > 1e-12 || std::abs(roll - link.roll) > 1e-6}; + if (false == changed && false == link.moved) + continue; + link.moved = true; + auto const c{glm::dot(from, to)}; + auto const s{from.x * to.z - from.z * to.x}; + auto const turn = [&](glm::dvec3 const &Vector) { return glm::dvec3{Vector.x * c - Vector.z * s, Vector.y, Vector.z * c + Vector.x * s}; }; + auto const tilt = [&](glm::dvec3 const &Vector) { return (Vector.x * to.x + Vector.z * to.z) * grade; }; + for (auto &member : link.cluster) + { + member.first->m_paths = member.second; + for (auto &path : member.first->m_paths) + { + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const moved{turn(path.points[index] - link.point)}; + path.points[index] = point + moved + glm::dvec3{0.0, tilt(moved), 0.0}; + } + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + { + auto &control{path.points[index]}; + if (control == glm::dvec3{}) + continue; + control = turn(control); + control.y += tilt(control); + } + } + } + if (std::abs(roll - link.roll) > 1e-6) + { + auto &main{link.track->m_paths[0]}; + bool const start{link.theirs.kind == editor_track::point_kind::start}; + auto value{start ? -roll : roll}; + if (link.theirs.path != 0) + { + auto const t{start ? 0.0 : 1.0}; + if (glm::dot(plan_of(bezier{link.track->m_paths[link.theirs.path]}.first(t)), plan_of(bezier{main}.first(t))) < 0.0) + value = -value; + } + main.rolls = {static_cast(value), static_cast(value)}; + editor_track::conform_switch(*link.track); + } + for (auto const &entry : link.joints) + { + editor_track::follow(*entry.first, entry.second); + editor_track::follow_grade(*entry.first, entry.second); + match_rolls(*entry.first, entry.second); + } + } } void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_operation const Operation) @@ -803,28 +874,40 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o m_track_drag.emplace_back(joint.other); } } - if (Operation == gizmo_operation::rotate && m_track_drag_connected) + if (m_track_drag_connected) { + auto const taken = [&](TTrack const *Other) { return std::any_of(drag.switches.begin(), drag.switches.end(), [&](auto const &Link) { return std::any_of(Link.cluster.begin(), Link.cluster.end(), [&](auto const &Member) { return Member.first == Other; }); }); }; + auto const movable = [&](TTrack const *Other) { + std::string reason; + return Other != &Track && Other->eType == tt_Switch && Other->m_paths.size() == 2 && editor_track::can_edit_geometry(*Other, reason) && false == taken(Other); + }; for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) { editor_track::point_ref const own{i, kind}; for (auto const &connection : editor_track::connected_points(Track, editor_track::point_position(Track, own))) { - auto *other{connection.first}; - std::string reason; - if (other->eType != tt_Switch || other->m_paths.size() != 2 || false == editor_track::can_edit_geometry(*other, reason)) + if (false == movable(connection.first)) continue; - if (std::any_of(drag.switches.begin(), drag.switches.end(), [&](auto const &Link) { return Link.track == other; })) - continue; - auto joints{editor_track::joints(*other)}; - joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return Joint.other == &Track; }), joints.end()); - if (std::find(m_track_drag.begin(), m_track_drag.end(), other) == m_track_drag.end()) - m_track_drag.emplace_back(other); - for (auto const &joint : joints) - if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) - m_track_drag.emplace_back(joint.other); - drag.switches.push_back({other, own, connection.second, roll_into(Track, own), std::move(joints)}); + track_gizmo_drag::switch_joint link{connection.first, own, connection.second, editor_track::point_position(Track, own), track_outward(Track, own), roll_into(Track, own)}; + std::vector members{connection.first}; + for (std::size_t k = 0; k < members.size(); ++k) + for (auto *next : editor_track::neighbours(*members[k])) + if (movable(next) && std::find(members.begin(), members.end(), next) == members.end()) + members.push_back(next); + for (auto *member : members) + { + link.cluster.emplace_back(member, std::vector{}); + auto joints{editor_track::joints(*member)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return Joint.other == &Track; }), joints.end()); + if (std::find(m_track_drag.begin(), m_track_drag.end(), member) == m_track_drag.end()) + m_track_drag.emplace_back(member); + for (auto const &joint : joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.emplace_back(joint.other); + link.joints.emplace_back(member, std::move(joints)); + } + drag.switches.push_back(std::move(link)); } } } @@ -833,6 +916,9 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o states.emplace_back(dragged, editor_track::capture(*dragged)); push_track_snapshot(std::move(states)); track_gizmo_hold(); + for (auto &link : drag.switches) + for (auto &member : link.cluster) + member.second = member.first->m_paths; if (controlmode) { drag.origin = editor_track::point_position(Track, m_track_point); @@ -966,26 +1052,6 @@ void editor_mode::track_gizmo_turn(TTrack &Track, int const Axis, double const A editor_track::follow_grade(Track, m_track_joints); } match_rolls(Track, m_track_joints); - for (auto const &link : drag.switches) - { - auto const roll{roll_into(Track, link.own)}; - if (std::abs(roll - link.roll) < 1e-6) - continue; - auto &main{link.track->m_paths[0]}; - bool const start{link.theirs.kind == editor_track::point_kind::start}; - auto value{start ? -roll : roll}; - if (link.theirs.path != 0) - { - auto const t{start ? 0.0 : 1.0}; - if (glm::dot(plan_of(bezier{link.track->m_paths[link.theirs.path]}.first(t)), plan_of(bezier{main}.first(t))) < 0.0) - value = -value; - } - main.rolls = {static_cast(value), static_cast(value)}; - editor_track::conform_switch(*link.track); - editor_track::follow(*link.track, link.joints); - editor_track::follow_grade(*link.track, link.joints); - match_rolls(*link.track, link.joints); - } for (auto const &partner : drag.partners) { if (partner.first == &Track) @@ -1123,6 +1189,7 @@ void editor_mode::render_track_gizmo() m_track_dirty = true; } } + track_gizmo_switches(*track); commit_track_drag(false); } else if (m_track_gizmo_using) From 572daeb274e14ca04128c5a4a513f4e072fc4563 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 16:06:23 +0200 Subject: [PATCH 015/150] feat(editor): the horizontal alignment designed in the route design kept in the scenery as //$h lines: its vertices and the elements they make (straight, transition, arc, with the radius at the start and the end and the cant), so the paths laid by it are known again without guessing from their curvature; a curve read from the paths takes the radius, the transitions, the cant and the compound arcs of the kept design when its paths lie on it within 2 cm, the other way round too; the whole line design taken back when the fragment has its ends; a curve redesigned alone taken into the line it belongs to; km of the start kept with the design and carried on to the vertical profile of the same paths; paths which no longer follow the kept design shown in the route window, with the kept design to take or to forget; self-test commands for the route design and the kept alignments; Polish translation --- CMakeLists.txt | 3 +- application/editormode.cpp | 2 + application/editormode.h | 10 +- application/editormodeprofile.cpp | 10 + application/editormodeselftest.cpp | 207 ++++++++++ application/editormodetrack.cpp | 117 +++++- editor/editorAlignmentStore.cpp | 610 +++++++++++++++++++++++++++++ editor/editorAlignmentStore.hpp | 91 +++++ editor/editorTrack.cpp | 21 + editor/editorTrack.hpp | 4 + lang/editor_pl.po | 33 ++ scene/scenelayers.cpp | 2 +- 12 files changed, 1105 insertions(+), 5 deletions(-) create mode 100644 editor/editorAlignmentStore.cpp create mode 100644 editor/editorAlignmentStore.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 62b41aa5..7d6b7d24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -178,6 +178,7 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorRoad.cpp" "editor/editorAlignment.cpp" +"editor/editorAlignmentStore.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" @@ -698,4 +699,4 @@ if(ENABLE_AVX2) else() target_compile_options(${PROJECT_NAME} PRIVATE -mavx2) endif() -endif() \ No newline at end of file +endif() diff --git a/application/editormode.cpp b/application/editormode.cpp index be970a88..134c5335 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -28,6 +28,7 @@ extern char **environ; #include "editor/editorIncludeInfo.hpp" #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIcons.hpp" #include "utilities/translation.h" @@ -495,6 +496,7 @@ void editor_mode::editormode_input::poll() } editor_mode::editor_mode() { + alignment_store::reset(); m_userinterface = std::make_shared(); // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); diff --git a/application/editormode.h b/application/editormode.h index 2c6d65e9..015497bd 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -769,6 +769,10 @@ class editor_mode : public application_mode, private editor_track::observer int vertex{-1}; int grip{-1}; bool single{true}; // one curve between its straights, instead of the whole line + double origin{0.0}; // km of the start, m + int designed{0}; // vertices whose parameters came from the alignment kept in the scenery + int departed{-1}; // alignment kept in the scenery near the fragment, which its paths no longer follow + double departure{0.0}; }; void render_route_ui(); bool render_route_parameters(); @@ -780,7 +784,11 @@ class editor_mode : public application_mode, private editor_track::observer bool route_from_curve(TTrack &Track); bool route_from_line(TTrack &Track); alignment::vertex route_vertex_of(editor_track::curve const &Curve) const; - bool route_load(TTrack &Track) { return m_route.single ? route_from_curve(Track) : route_from_line(Track); } + bool route_load(TTrack &Track); + void route_read_stored(); + void route_take_stored(int const Index); + void route_store(); + void render_route_stored(); void route_update() { m_route.result = alignment::compute(m_route.design); } void route_apply(); bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created); diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp index 4fafe1d3..eb10e686 100644 --- a/application/editormodeprofile.cpp +++ b/application/editormodeprofile.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "application/editormode.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" #include "world/Track.h" @@ -390,7 +391,16 @@ void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack * state.selected = -1; profile_resample(); if (false == profile_restore()) + { profile_recognize(); + if (false == state.samples.empty()) + { + auto const km{alignment_store::chainage_at(state.samples.front().position)}; + auto const further{alignment_store::chainage_at(editor_track::sampled_position(state.samples, std::min(state.route.length, 5.0)))}; + if (km.has_value() && further.has_value() && *further > *km) + state.origin = *km; + } + } profile_fit_view(); } diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 07929123..50558c8c 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -33,6 +33,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include #include @@ -92,6 +93,11 @@ http://mozilla.org/MPL/2.0/. // layto from the free end at the first point towards the second, as a drag in the layout // layclicks ... clicks of the lay tool at the points, a free end within 3 m joined // joints ... the joints check over the scenery, the issues around the points logged +// route the route design of the path nearest the point, a curve or the whole line +// routevertex , routearc , routeshare , routekm +// routeapply, routetake as the buttons of the route design do it +// alignments logs the alignments kept in the scenery and the element under each path +// splitkind , nudgekind the first path over an element of the kind split in two, or moved // screenshot // save // log @@ -977,6 +983,207 @@ void editor_mode::selftest_step() else WriteLog("SELFTEST gauge " + name + ": no such track"); } + else if (command == "route") + { + glm::dvec3 point{0.0}; + int single{1}; + words >> point.x >> point.z >> single; + TTrack *nearest{nullptr}; + double distance{1e9}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)}; + if (d < distance) + { + distance = d; + nearest = track; + } + } + m_track_tab = track_tab::route; + m_route.single = (single != 0); + auto const loaded{nearest != nullptr && route_load(*nearest)}; + WriteLog(format("SELFTEST route: %s from %s, %zu paths, %.3f m, %zu vertices, %d from the design, km %.3f, departed %d by %.3f m, valid %d", loaded ? "loaded" : "failed", nearest != nullptr ? nearest->name().c_str() : "-", + m_route.chain.tracks.size(), m_route.chain.length, m_route.design.vertices.size(), m_route.designed, m_route.origin / 1000.0, m_route.departed, m_route.departure, m_route.result.valid ? 1 : 0)); + WriteLog("SELFTEST route status: " + m_route.status + " " + m_route.error); + for (std::size_t i = 0; i < m_route.design.vertices.size(); ++i) + { + auto const &v{m_route.design.vertices[i]}; + auto text{format("SELFTEST route vertex %zu: at %.3f %.3f offset %.3f R %.3f L %.3f / %.3f cant %.1f compound %d share %.4f", i, v.position.x, v.position.y, v.offset, v.radius, v.transition_in, v.transition_out, v.cant, v.compound ? 1 : 0, v.share)}; + for (auto const &arc : v.arcs) + text += format(" [R %.3f L %.3f share %.4f]", arc.radius, arc.transition, arc.share); + WriteLog(text); + } + } + else if (command == "routevertex" || command == "routearc" || command == "routeshare") + { + int index{0}; + words >> index; + if (index < 0 || index >= static_cast(m_route.design.vertices.size())) + { + WriteLog("SELFTEST " + command + ": no vertex " + std::to_string(index)); + continue; + } + auto &vertex{m_route.design.vertices[index]}; + if (command == "routevertex") + words >> vertex.radius >> vertex.transition_in >> vertex.transition_out >> vertex.cant; + else if (command == "routeshare") + words >> vertex.share; + else + { + alignment::compound_arc arc; + words >> arc.radius >> arc.transition >> arc.share; + vertex.compound = true; + vertex.arcs.push_back(arc); + } + route_update(); + std::string errors; + for (auto const &error : m_route.result.errors) + errors += " " + error; + WriteLog(format("SELFTEST %s: valid %d, %zu elements, extension %.3f / %.3f", command.c_str(), m_route.result.valid ? 1 : 0, m_route.result.elements.size(), m_route.result.start_extension, m_route.result.end_extension) + errors); + } + else if (command == "routekm") + { + double km{0.0}; + words >> km; + m_route.origin = km * 1000.0; + } + else if (command == "routeapply" || command == "routetake") + { + if (command == "routeapply") + route_apply(); + else + route_take_stored(m_route.departed); + WriteLog("SELFTEST " + command + ": " + m_route.status + " " + m_route.error); + } + else if (command == "alignments") + { + auto const &records{alignment_store::records()}; + for (std::size_t r = 0; r < records.size(); ++r) + { + auto const &entry{records[r]}; + WriteLog(format("SELFTEST alignment %zu in %s: km %.3f, %zu vertices, %zu elements, consistent %d, length %.3f", r, scene::Layers.layer(entry.layer).name.c_str(), entry.origin / 1000.0, entry.design.vertices.size(), entry.elements.size(), entry.consistent ? 1 : 0, entry.result.length)); + for (auto const &e : entry.elements) + WriteLog(format("SELFTEST element %s vertex %d km %.3f L %.3f R %.3f -> %.3f cant %.1f", e.kind.c_str(), e.vertex, (entry.origin + e.chainage) / 1000.0, e.length, e.radius_start, e.radius_end, e.cant)); + } + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + std::string text{"SELFTEST path " + track->name() + ": "}; + if (false == found.has_value()) + { + auto const closest{alignment_store::at(geometry::bezier{track->m_paths.front()}.point(0.5), 1000.0)}; + text += closest.has_value() ? format("off the alignments, %.3f m from alignment %d", closest->offset, closest->record) : std::string{"off the alignments"}; + } + else + { + auto const e{alignment_store::describe(records[found->record].result.elements[found->element])}; + text += format("alignment %d element %d %s R %.3f -> %.3f, offset %.4f", found->record, found->element, e.kind.c_str(), e.radius_start, e.radius_end, found->offset); + } + editor_track::curve curve; + if (editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + text += format(" | curve R %.3f L %.3f / %.3f cant %.1f compound %d arcs %zu %s", curve.radius, curve.transition_in, curve.transition_out, curve.cant, curve.compound ? 1 : 0, curve.arcs.size(), curve.designed.has_value() ? "designed" : "recognized"); + WriteLog(text); + } + } + else if (command == "vprofile") + { + std::string from, to; + words >> from >> to; + auto *first{simulation::Paths.find(from)}; + auto *last{simulation::Paths.find(to)}; + if (first == nullptr || last == nullptr) + { + WriteLog("SELFTEST vprofile: no such paths"); + continue; + } + m_profile.origin = 0.0; + profile_open(first, last); + WriteLog(format("SELFTEST vprofile: %zu spans, %.3f m, km of the start %.3f", m_profile.route.spans.size(), m_profile.route.length, m_profile.origin / 1000.0)); + } + else if (command == "backwardcurve") + { + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (track == nullptr || false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve) || false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + { + WriteLog("SELFTEST backwardcurve: no curve at " + name); + continue; + } + std::vector> run; + for (std::size_t i = chain.tracks.size(); i > 0; --i) + run.emplace_back(chain.tracks[i - 1], false == chain.forward[i - 1]); + editor_track::curve backward; + editor_track::analyse_curve(run, m_route.design.norms.gauge, backward); + auto text{format("SELFTEST backwardcurve: %zu paths, R %.3f L %.3f / %.3f cant %.1f compound %d %s", run.size(), backward.radius, backward.transition_in, backward.transition_out, backward.cant, backward.compound ? 1 : 0, backward.designed.has_value() ? "designed" : "recognized")}; + for (auto const &arc : backward.arcs) + text += format(" [R %.3f turn %.4f L %.3f]", arc.radius, arc.turn, arc.transition); + WriteLog(text); + } + else if (command == "splitkind" || command == "nudgekind") + { + std::string kind; + glm::dvec3 offset{0.0}; + words >> kind >> offset.x >> offset.z; + auto const &records{alignment_store::records()}; + TTrack *chosen{nullptr}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty()) + continue; + auto const found{alignment_store::on_track(*track)}; + if (found.has_value() && alignment_store::describe(records[found->record].result.elements[found->element]).kind == kind) + { + chosen = track; + break; + } + } + if (chosen == nullptr) + { + WriteLog("SELFTEST " + command + ": no path over " + kind); + continue; + } + if (command == "splitkind") + { + auto const before{editor_track::capture(*chosen)}; + auto *created{editor_track::split_path(*chosen, 0.5)}; + if (created != nullptr) + push_track_snapshot({{chosen, before}}, {created}); + WriteLog("SELFTEST splitkind: " + chosen->name() + " split, the second half " + (created != nullptr ? created->name() : std::string{"-"})); + } + else + { + std::vector> states{{chosen, editor_track::capture(*chosen)}}; + std::vector moved{chosen}; + auto const &path{chosen->m_paths.front()}; + for (auto const &end : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) + for (auto *neighbour : {chosen->trPrev, chosen->trNext}) + { + if (neighbour == nullptr || neighbour->m_paths.empty() || std::find(moved.begin(), moved.end(), neighbour) != moved.end()) + continue; + auto const &other{neighbour->m_paths.front()}; + for (auto const kind : {editor_track::point_kind::start, editor_track::point_kind::end}) + if (glm::distance(other.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end], end) < 0.01) + { + states.emplace_back(neighbour, editor_track::capture(*neighbour)); + editor_track::move_point(*neighbour, {0, kind}, end + offset); + moved.push_back(neighbour); + break; + } + } + editor_track::translate(*chosen, offset); + editor_track::commit(moved); + push_track_snapshot(std::move(states), {}); + WriteLog("SELFTEST nudgekind: " + chosen->name() + " moved with " + std::to_string(moved.size() - 1) + " joined ends"); + } + } else if (command == "join" || command == "layto") { glm::dvec3 a{0.0}, b{0.0}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 9cf99544..c5a0474c 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -13,6 +13,7 @@ http: #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" @@ -2411,6 +2412,7 @@ bool editor_mode::route_from_curve(TTrack &Track) return false; auto &design{m_route.design}; auto vertex{route_vertex_of(curve)}; + m_route.designed = curve.designed.has_value() ? 1 : 0; if (curve.reversals == 0) { design.vertices = {vertex}; @@ -2430,6 +2432,8 @@ bool editor_mode::route_from_curve(TTrack &Track) alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const { + if (Curve.designed.has_value()) + return *Curve.designed; alignment::vertex vertex; vertex.radius = Curve.radius > 0.0 ? std::max(10.0, std::round(Curve.radius)) : 1000.0; route_recommend(vertex); @@ -2444,6 +2448,110 @@ alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) return vertex; } +bool editor_mode::route_load(TTrack &Track) +{ + m_route.designed = 0; + if (false == (m_route.single ? route_from_curve(Track) : route_from_line(Track))) + return false; + route_read_stored(); + return true; +} + +// the design kept in the scenery for the fragment takes the place of the one read from the paths; its chainage is carried on +void editor_mode::route_read_stored() +{ + auto &route{m_route}; + route.departed = -1; + route.departure = 0.0; + route.origin = 0.0; + if (route.chain.tracks.empty()) + return; + auto const &chain{route.chain}; + bool reversed; + auto const index{alignment_store::find(chain.start, chain.end, reversed)}; + if (index >= 0 && false == reversed && false == route.single) + { + route_take_stored(index); + } + else if (auto const km{alignment_store::chainage_at(chain.start)}) + { + auto const ahead{chain.start + glm::dvec3{route.design.start_direction.x, 0.0, route.design.start_direction.y}}; + auto const further{alignment_store::chainage_at(ahead)}; + if (further.has_value() && *further > *km) + route.origin = *km; + } + route.departed = alignment_store::departed(chain.tracks, route.departure); + if (route.designed > 0) + route.status += format(STR_C("\nCurves read from the design kept in the scenery: %d"), route.designed); + else if (false == route.design.vertices.empty()) + route.status += STR_C("\nCurves recognized from the paths, the scenery keeps no design for them"); +} + +void editor_mode::route_take_stored(int const Index) +{ + auto &route{m_route}; + auto const &records{alignment_store::records()}; + if (Index < 0 || Index >= static_cast(records.size())) + return; + auto const &stored{records[Index]}; + auto &design{route.design}; + design.vertices = stored.design.vertices; + design.shape = stored.design.shape; + design.speed = stored.design.speed; + design.norms = stored.design.norms; + design.transition_pieces = stored.design.transition_pieces; + design.arc_piece_angle = stored.design.arc_piece_angle; + route.designed = static_cast(design.vertices.size()); + route.origin = stored.origin; + route.vertex = route.grip = -1; + route_update(); +} + +void editor_mode::render_route_stored() +{ + auto &route{m_route}; + ImGui::PushItemWidth(120.0f); + auto km{route.origin / 1000.0}; + if (ImGui::InputDouble(STR_C("Km of the start"), &km, 0.0, 0.0, "%.3f")) + route.origin = km * 1000.0; + ImGui::PopItemWidth(); + item_tooltip("Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it"); + auto const &records{alignment_store::records()}; + if (route.departed < 0 || route.departed >= static_cast(records.size())) + return; + auto const &layer{scene::Layers.layer(records[route.departed].layer).name}; + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The design kept in %s for this fragment departs from its paths by up to %.2f m"), layer.c_str(), route.departure); + if (ImGui::SmallButton(STR_C("Take the kept design"))) + { + route_take_stored(route.departed); + route.status = STR_C("The kept design is loaded, applying it lays the paths by it again"); + } + item_tooltip("Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied"); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Forget the kept design"))) + { + alignment_store::forget(route.departed); + route.departed = -1; + route.status = STR_C("The kept design will be removed from ") + layer + STR_C(" on save"); + } + item_tooltip("The paths stay as they are, the scenery no longer keeps a design for them"); +} + +void editor_mode::route_store() +{ + auto &route{m_route}; + if (false == route.result.valid || route.chain.tracks.empty() || scene::Layers.empty()) + return; + auto layer{scene::Layers.resolve(route.chain.tracks.front()->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + alignment_store::store(route.design, route.result, route.origin, layer); + route.departed = -1; + route.departure = 0.0; +} + bool editor_mode::route_from_line(TTrack &Track) { auto &route{m_route}; @@ -2457,6 +2565,7 @@ bool editor_mode::route_from_line(TTrack &Track) return false; auto &design{route.design}; auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge)}; + route.designed = 0; if (false == found.empty()) { design.vertices.clear(); @@ -2471,6 +2580,8 @@ bool editor_mode::route_from_line(TTrack &Track) else { vertex = route_vertex_of(entry.shape); + if (entry.shape.designed.has_value()) + ++route.designed; } vertex.position = entry.position; design.vertices.push_back(vertex); @@ -2583,6 +2694,7 @@ void editor_mode::route_apply() } route_update(); } + route_store(); route.status = STR_C("Re-laid ") + std::to_string(chain.tracks.size()) + STR_C(" paths") + (added > 0 ? STR_C(", added ") + std::to_string(added) : std::string{}); if (startextension > 0.0 || endextension > 0.0) route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); @@ -2935,6 +3047,7 @@ void editor_mode::render_route_ui() return; } item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); + render_route_stored(); auto changed{render_route_vertices()}; if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) changed |= render_route_parameters(); @@ -3115,9 +3228,9 @@ void editor_mode::render_route_result() if (ImGui::TreeNode(STR_C("Elements"))) { ImGui::TextDisabled(STR_C("Straight available beyond the ends: %.2f / %.2f m"), design.start_reserve, design.end_reserve); - char const *kinds[] = {"straight", "transition", "curve", "transition"}; + char const *kinds[] = {"straight", "transition", "curve", "transition", "spiral"}; for (auto const &e : result.elements) - ImGui::Text("%8.2f m %-10s L %8.2f m%s", e.chainage, kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); + ImGui::Text("km %s %-10s L %8.2f m%s", profile::format_chainage(m_route.origin + e.chainage).c_str(), kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); ImGui::TreePop(); } diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp new file mode 100644 index 00000000..64cc86f7 --- /dev/null +++ b/editor/editorAlignmentStore.cpp @@ -0,0 +1,610 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "world/Track.h" +#include "utilities/Logs.h" + +#include +#include +#include +#include +#include + +namespace alignment_store +{ + +namespace +{ + +using geometry::plan_of; + +std::string const kMark{"//$h"}; +double const kStep{1.0}; + +std::vector g_records; +bool g_loaded{false}; + +// format of the lines, coordinates in the space of the file: +// alignment +// source origin shape speed +// ends +// norms +// pieces +// vertex [ ]... +// element +// radii signed with the turn, 0 for no curvature +// end +char const *shape_name(alignment::transition_shape const Shape) +{ + return Shape == alignment::transition_shape::clothoid ? "clothoid" : "parabola"; +} + +glm::dvec2 local_direction(scene::layer_context const &Context, glm::dvec3 const &Point, glm::dvec2 const &Direction) +{ + auto const from{Context.to_local(Point)}; + auto const to{Context.to_local(Point + glm::dvec3{Direction.x, 0.0, Direction.y})}; + auto const direction{plan_of(to - from)}; + return glm::length(direction) > 1e-12 ? glm::normalize(direction) : Direction; +} + +glm::dvec2 world_direction(scene::layer_context const &Context, glm::dvec3 const &Local, glm::dvec2 const &Direction) +{ + auto const from{Context.to_world(Local)}; + auto const to{Context.to_world(Local + glm::dvec3{Direction.x, 0.0, Direction.y})}; + auto const direction{plan_of(to - from)}; + return glm::length(direction) > 1e-12 ? glm::normalize(direction) : Direction; +} + +void sample(record &Record) +{ + Record.samples.clear(); + auto const length{Record.result.length}; + auto const count{static_cast(std::ceil(length / kStep))}; + for (std::size_t i = 0; i <= count; ++i) + Record.samples.push_back(plan_of(alignment::evaluate(Record.result, std::min(length, i * kStep)).position)); + Record.low = Record.high = Record.samples.empty() ? glm::dvec2{0.0} : Record.samples.front(); + for (auto const &point : Record.samples) + { + Record.low = glm::min(Record.low, point); + Record.high = glm::max(Record.high, point); + } +} + +bool agrees(std::vector const &Stored, std::vector const &Made) +{ + if (Stored.size() != Made.size()) + return false; + for (std::size_t i = 0; i < Stored.size(); ++i) + { + auto const made{describe(Made[i])}; + if (Stored[i].kind != made.kind || std::abs(Stored[i].chainage - made.chainage) > 0.01 || std::abs(Stored[i].length - made.length) > 0.01 || + std::abs(Stored[i].radius_start - made.radius_start) > 0.01 || std::abs(Stored[i].radius_end - made.radius_end) > 0.01) + return false; + } + return true; +} + +void mark(scene::layer_handle const Layer) +{ + if (false == scene::Layers.valid(Layer)) + return; + auto const &context{scene::Layers.layer(Layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + std::vector lines; + for (auto const &entry : g_records) + { + if (entry.layer != Layer) + continue; + if (lines.empty()) + lines.emplace_back("# horizontal alignments designed in the track editor, the simulation skips these lines"); + auto const &design{entry.design}; + lines.push_back("alignment " + local(design.start) + ' ' + local(design.end)); + lines.push_back(format("source %s origin %.4f shape %s speed %.1f", entry.source.c_str(), entry.origin, shape_name(design.shape), design.speed)); + auto const startdirection{local_direction(context, design.start, design.start_direction)}; + auto const enddirection{local_direction(context, design.end, design.end_direction)}; + lines.push_back(format("ends %.9f %.9f %.9f %.9f %.6f %.6f %.3f %.3f", startdirection.x, startdirection.y, enddirection.x, enddirection.y, design.start_grade, design.end_grade, design.start_radius, design.end_radius)); + auto const &norms{design.norms}; + lines.push_back(format("norms %.1f %.3f %.1f %.1f %.2f %.3f %.2f %.2f %.3f", norms.gauge, norms.unbalanced, norms.cant_max, norms.cant_rate, norms.ramp_factor, norms.jerk, norms.tangent_min_time, norms.arc_min, norms.arc_min_factor)); + lines.push_back(format("pieces %d %.3f", design.transition_pieces, design.arc_piece_angle)); + for (auto const &vertex : design.vertices) + { + auto const position{context.to_local({vertex.position.x, 0.0, vertex.position.y})}; + auto text{format("vertex %.4f %.4f %.4f %.3f %.3f %.3f %.1f %d %d %.9f %zu", position.x, position.z, vertex.offset, vertex.radius, vertex.transition_in, vertex.transition_out, vertex.cant, vertex.reverse_turn ? 1 : 0, vertex.compound ? 1 : 0, + vertex.share, vertex.arcs.size())}; + for (auto const &arc : vertex.arcs) + text += format(" %.3f %.3f %.9f", arc.radius, arc.transition, arc.share); + lines.push_back(text); + } + for (auto const &e : entry.result.elements) + { + auto const station{alignment::evaluate(entry.result, e.chainage)}; + auto const start{context.to_local(station.position)}; + auto const direction{local_direction(context, station.position, glm::normalize(plan_of(station.direction)))}; + auto const element{describe(e)}; + lines.push_back(format("element %s %d %.4f %.4f %.3f %.3f %.1f %.4f %.4f %.9f %.9f", element.kind.c_str(), element.vertex, element.chainage, element.length, element.radius_start, element.radius_end, element.cant, start.x, start.z, direction.x, direction.y)); + } + lines.emplace_back("end"); + } + scene::Layers.mark(Layer, kMark, std::move(lines)); +} + +// the vertex as seen from the other end of the line +alignment::vertex reversed_vertex(alignment::vertex Vertex) +{ + std::swap(Vertex.transition_in, Vertex.transition_out); + if (Vertex.compound && false == Vertex.arcs.empty()) + { + struct part + { + double radius, share, transition; + }; + std::vector parts{{Vertex.radius, Vertex.share, 0.0}}; + for (auto const &arc : Vertex.arcs) + parts.push_back({arc.radius, arc.share, arc.transition}); + auto const count{parts.size()}; + Vertex.radius = parts.back().radius; + Vertex.share = parts.back().share; + for (std::size_t j = 1; j < count; ++j) + Vertex.arcs[j - 1] = {parts[count - 1 - j].radius, parts[count - j].transition, parts[count - 1 - j].share}; + } + return Vertex; +} + +void finish(record &Record) +{ + Record.result = alignment::compute(Record.design); + sample(Record); +} + +// the curves of a design whose vertices are vertices of a stored alignment are taken into that alignment, in place of its own +int merge(record const &Entry) +{ + auto const &vertices{Entry.result.vertices}; + if (vertices.empty()) + return -1; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const &others{g_records[r].result.vertices}; + if (others.size() < vertices.size()) + continue; + for (auto const backwards : {false, true}) + for (std::size_t first = 0; first + vertices.size() <= others.size(); ++first) + { + auto const other = [&](std::size_t const K) { return backwards ? first + vertices.size() - 1 - K : first + K; }; + bool same{true}; + for (std::size_t k = 0; k < vertices.size() && same; ++k) + same = glm::distance(vertices[k], others[other(k)]) <= 0.1; + if (false == same) + continue; + auto merged{g_records[r]}; + for (std::size_t k = 0; k < vertices.size(); ++k) + { + auto const shape{backwards ? reversed_vertex(Entry.design.vertices[k]) : Entry.design.vertices[k]}; + auto &target{merged.design.vertices[other(k)]}; + auto const position{target.position}; + auto const offset{target.offset}; + target = shape; + target.position = position; + target.offset = offset; + } + finish(merged); + if (false == merged.result.valid) + return -1; + merged.elements.clear(); + for (auto const &e : merged.result.elements) + merged.elements.push_back(describe(e)); + merged.consistent = true; + g_records[r] = std::move(merged); + return static_cast(r); + } + } + return -1; +} + +} // namespace + +stored_element describe(alignment::element const &Element) +{ + stored_element result; + result.vertex = Element.vertex; + result.chainage = Element.chainage; + result.length = Element.length; + result.cant = Element.cant; + auto const sign{Element.turn < 0 ? -1.0 : 1.0}; + auto const radius = [&](double const Curvature) { return Curvature > 1e-12 ? sign / Curvature : 0.0; }; + switch (Element.kind) + { + case alignment::element_kind::transition_in: + result.radius_end = sign * Element.radius; + break; + case alignment::element_kind::arc: + result.radius_start = result.radius_end = sign * Element.radius; + break; + case alignment::element_kind::transition_out: + result.radius_start = sign * Element.radius; + break; + case alignment::element_kind::spiral: + result.radius_start = radius(Element.curvature_start); + result.radius_end = radius(Element.curvature_end); + break; + default: + result.cant = 0.0; + break; + } + if (result.radius_start == 0.0 && result.radius_end == 0.0) + result.kind = "straight"; + else if (std::abs(result.radius_start - result.radius_end) < 1e-6) + result.kind = "arc"; + else + result.kind = "transition"; + return result; +} + +void reset() +{ + g_records.clear(); + g_loaded = false; +} + +void load() +{ + if (g_loaded || scene::Layers.empty()) + return; + g_loaded = true; + g_records.clear(); + for (auto const layer : scene::Layers.marked_layers(kMark)) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + std::optional current; + std::vector made; + for (auto const &text : scene::Layers.marked(layer, kMark)) + { + std::istringstream line{text}; + std::string keyword, word; + line >> keyword; + if (keyword == "alignment") + { + record entry; + entry.layer = layer; + glm::dvec3 start, end; + line >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z; + if (line.fail()) + { + current.reset(); + continue; + } + entry.design.start = context.to_world(start); + entry.design.end = context.to_world(end); + current = entry; + continue; + } + if (false == current.has_value()) + continue; + auto &entry{*current}; + auto &design{entry.design}; + if (keyword == "source") + { + std::string shape; + line >> entry.source >> word >> entry.origin >> word >> shape >> word >> design.speed; + design.shape = shape == "clothoid" ? alignment::transition_shape::clothoid : alignment::transition_shape::cubic_parabola; + } + else if (keyword == "ends") + { + glm::dvec2 startdirection, enddirection; + line >> startdirection.x >> startdirection.y >> enddirection.x >> enddirection.y >> design.start_grade >> design.end_grade >> design.start_radius >> design.end_radius; + design.start_direction = world_direction(context, context.to_local(design.start), startdirection); + design.end_direction = world_direction(context, context.to_local(design.end), enddirection); + } + else if (keyword == "norms") + { + auto &norms{design.norms}; + line >> norms.gauge >> norms.unbalanced >> norms.cant_max >> norms.cant_rate >> norms.ramp_factor >> norms.jerk >> norms.tangent_min_time >> norms.arc_min >> norms.arc_min_factor; + } + else if (keyword == "pieces") + { + line >> design.transition_pieces >> design.arc_piece_angle; + } + else if (keyword == "vertex") + { + alignment::vertex vertex; + glm::dvec3 position{0.0}; + int reverse{0}, compound{0}; + std::size_t arcs{0}; + line >> position.x >> position.z >> vertex.offset >> vertex.radius >> vertex.transition_in >> vertex.transition_out >> vertex.cant >> reverse >> compound >> vertex.share >> arcs; + for (std::size_t i = 0; i < arcs && false == line.fail(); ++i) + { + alignment::compound_arc arc; + line >> arc.radius >> arc.transition >> arc.share; + vertex.arcs.push_back(arc); + } + if (line.fail()) + continue; + position = context.to_world(position); + vertex.position = {position.x, position.z}; + vertex.reverse_turn = (reverse != 0); + vertex.compound = (compound != 0); + design.vertices.push_back(vertex); + } + else if (keyword == "element") + { + stored_element e; + line >> e.kind >> e.vertex >> e.chainage >> e.length >> e.radius_start >> e.radius_end >> e.cant; + if (false == line.fail()) + entry.elements.push_back(e); + } + else if (keyword == "end") + { + finish(entry); + if (false == entry.result.valid || entry.result.elements.empty()) + { + WriteLog("Editor: a horizontal alignment in " + scene::Layers.layer(layer).name + " doesn't make a line, left out", logtype::generic); + current.reset(); + continue; + } + entry.consistent = agrees(entry.elements, entry.result.elements); + if (false == entry.consistent) + WriteLog("Editor: the elements of a horizontal alignment in " + scene::Layers.layer(layer).name + " differ from the ones its design makes now", logtype::generic); + g_records.push_back(std::move(entry)); + current.reset(); + } + } + } + if (false == g_records.empty()) + WriteLog("Editor: " + std::to_string(g_records.size()) + " horizontal alignment(s) read from the scenery", logtype::generic); +} + +std::vector const &records() +{ + load(); + return g_records; +} + +namespace +{ + +// the point on the line of the stored alignment, if it lies by it within the reach and not beyond its ends +std::optional project(std::size_t const Index, glm::dvec2 const &Point, double const Reach) +{ + auto const &entry{g_records[Index]}; + auto const margin{Reach + kStep}; + if (entry.samples.empty() || Point.x < entry.low.x - margin || Point.y < entry.low.y - margin || Point.x > entry.high.x + margin || Point.y > entry.high.y + margin) + return std::nullopt; + std::size_t nearest{0}; + double distance{std::numeric_limits::max()}; + for (std::size_t i = 0; i < entry.samples.size(); ++i) + { + auto const d{glm::distance(entry.samples[i], Point)}; + if (d < distance) + { + distance = d; + nearest = i; + } + } + if (distance > margin) + return std::nullopt; + auto const away = [&](double const S) { return glm::distance(plan_of(alignment::evaluate(entry.result, S).position), Point); }; + auto low{std::max(0.0, nearest * kStep - kStep)}; + auto high{std::min(entry.result.length, nearest * kStep + kStep)}; + for (int i = 0; i < 60; ++i) + { + auto const a{low + (high - low) / 3.0}; + auto const b{high - (high - low) / 3.0}; + if (away(a) < away(b)) + high = b; + else + low = a; + } + auto const chainage{(low + high) * 0.5}; + auto const offset{away(chainage)}; + if (offset > Reach) + return std::nullopt; + if (chainage < 1e-3 || chainage > entry.result.length - 1e-3) + { + auto const end{alignment::evaluate(entry.result, chainage)}; + auto const outward{glm::dot(Point - plan_of(end.position), glm::normalize(plan_of(end.direction))) * (chainage < 1e-3 ? -1.0 : 1.0)}; + if (outward > tolerance) + return std::nullopt; + } + auto const &elements{entry.result.elements}; + auto const found{std::upper_bound(elements.begin(), elements.end(), chainage, [](double const Value, alignment::element const &E) { return Value < E.chainage; })}; + return hit{static_cast(Index), static_cast(std::max(0, std::distance(elements.begin(), found) - 1)), chainage, offset}; +} + +} // namespace + +std::optional at(glm::dvec3 const &Point, double const Reach) +{ + load(); + std::optional best; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const found{project(r, plan_of(Point), Reach)}; + if (found.has_value() && (false == best.has_value() || found->offset < best->offset)) + best = found; + } + return best; +} + +std::optional on_track(TTrack const &Track, double const Reach) +{ + if (Track.m_paths.empty() || Track.m_editorremoved) + return std::nullopt; + load(); + geometry::bezier const curve{Track.m_paths.front()}; + std::optional best; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const middle{project(r, plan_of(curve.point(0.5)), Reach)}; + if (false == middle.has_value() || (best.has_value() && middle->offset >= best->offset)) + continue; + bool along{true}; + for (auto const t : {0.0, 0.25, 0.75, 1.0}) + along = along && project(r, plan_of(curve.point(t)), Reach).has_value(); + if (along) + best = middle; + } + return best; +} + +std::optional vertex_of(std::vector> const &Run) +{ + if (Run.empty() || records().empty()) + return std::nullopt; + auto const entering = [&](std::pair const &Member, bool const Atend) { + auto const &path{Member.first->m_paths.front()}; + return plan_of(path.points[(Member.second == Atend) ? segment_data::point::end : segment_data::point::start]); + }; + for (std::size_t r = 0; r < g_records.size(); ++r) + { + auto const &entry{g_records[r]}; + std::set vertices; + bool along{true}; + for (auto const &member : Run) + { + if (member.first == nullptr || member.first->m_paths.empty()) + return std::nullopt; + geometry::bezier const curve{member.first->m_paths.front()}; + std::vector hits; + for (auto const t : {0.0, 0.25, 0.5, 0.75, 1.0}) + if (auto const found{project(r, plan_of(curve.point(t)), tolerance)}) + hits.push_back(*found); + if (hits.size() < 5) + { + along = false; + break; + } + auto const from{std::min(hits.front().chainage, hits.back().chainage) + 0.01}; + auto const to{std::max(hits.front().chainage, hits.back().chainage) - 0.01}; + for (auto const &e : entry.result.elements) + if (e.kind != alignment::element_kind::straight && e.chainage < to && e.chainage + e.length > from && e.vertex >= 0) + vertices.insert(e.vertex); + } + if (false == along || vertices.size() != 1 || *vertices.begin() >= static_cast(entry.design.vertices.size())) + continue; + auto const start{project(r, entering(Run.front(), false), tolerance)}; + auto const end{project(r, entering(Run.back(), true), tolerance)}; + if (false == start.has_value() || false == end.has_value()) + continue; + designed_vertex result; + result.record = static_cast(r); + result.vertex = *vertices.begin(); + result.shape = entry.design.vertices[result.vertex]; + result.reversed = end->chainage < start->chainage; + if (result.reversed) + result.shape = reversed_vertex(result.shape); + return result; + } + return std::nullopt; +} + +int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed) +{ + load(); + Reversed = false; + for (std::size_t i = 0; i < g_records.size(); ++i) + { + auto const &design{g_records[i].design}; + if (geometry::plan_distance(design.start, Start) <= 0.1 && geometry::plan_distance(design.end, End) <= 0.1) + return static_cast(i); + if (geometry::plan_distance(design.start, End) <= 0.1 && geometry::plan_distance(design.end, Start) <= 0.1) + { + Reversed = true; + return static_cast(i); + } + } + return -1; +} + +int departed(std::vector const &Tracks, double &Departure) +{ + load(); + Departure = 0.0; + double const reach{5.0}; + int result{-1}; + for (auto *track : Tracks) + { + if (track == nullptr || track->m_paths.empty() || on_track(*track).has_value()) + continue; + geometry::bezier const curve{track->m_paths.front()}; + for (auto const t : {0.0, 0.25, 0.5, 0.75, 1.0}) + { + auto const found{at(curve.point(t), reach)}; + if (found.has_value() && found->offset > tolerance && found->offset > Departure) + { + Departure = found->offset; + result = found->record; + } + } + } + return result; +} + +int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer) +{ + load(); + record entry; + entry.design = Design; + entry.design.start_reserve = entry.design.end_reserve = 0.0; + entry.origin = Origin; + finish(entry); + if (false == entry.result.valid) + { + entry.result = Result; + sample(entry); + } + entry.elements.clear(); + for (auto const &e : entry.result.elements) + entry.elements.push_back(describe(e)); + bool reversed; + auto index{find(Design.start, Design.end, reversed)}; + if (index >= 0) + { + entry.layer = g_records[index].layer; + g_records[index] = std::move(entry); + } + else if ((index = merge(entry)) >= 0) + { + } + else + { + entry.layer = Layer; + g_records.push_back(std::move(entry)); + index = static_cast(g_records.size()) - 1; + } + mark(g_records[index].layer); + return index; +} + +void forget(int const Index) +{ + load(); + if (Index < 0 || Index >= static_cast(g_records.size())) + return; + auto const layer{g_records[Index].layer}; + g_records.erase(g_records.begin() + Index); + mark(layer); +} + +std::optional chainage_at(glm::dvec3 const &Point) +{ + auto const found{at(Point)}; + if (false == found.has_value()) + return std::nullopt; + return g_records[found->record].origin + found->chainage; +} + +} // namespace alignment_store diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp new file mode 100644 index 00000000..1392f9d1 --- /dev/null +++ b/editor/editorAlignmentStore.hpp @@ -0,0 +1,91 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "editor/editorAlignment.hpp" +#include "scene/scenelayers.h" + +class TTrack; + +// horizontal alignments designed in the track editor, kept in the scenery files as "//$h" comment lines: the design (its +// vertices) and the elements it makes, so the tracks laid by it are known for straights, transition curves and arcs +namespace alignment_store +{ + +// an element as the file gives it: radii signed with the turn, 0 for no curvature +struct stored_element +{ + std::string kind; // straight, transition, arc + int vertex{-1}; + double chainage{0.0}; + double length{0.0}; + double radius_start{0.0}; + double radius_end{0.0}; + double cant{0.0}; +}; +stored_element describe(alignment::element const &Element); + +struct record +{ + scene::layer_handle layer{null_handle}; + std::string source{"designed"}; + double origin{0.0}; // chainage of the start, m + alignment::design design; + alignment::result result; // made of the design + std::vector elements; // as written in the file + bool consistent{true}; // the design makes the elements written in the file + glm::dvec2 low{0.0}, high{0.0}; + std::vector samples; +}; + +struct hit +{ + int record{-1}; + int element{-1}; + double chainage{0.0}; // from the start of the record + double offset{0.0}; // sideways from its line +}; + +// the designed vertex a curve made of the tracks follows: its parameters in the direction of the tracks +struct designed_vertex +{ + int record{-1}; + int vertex{-1}; + alignment::vertex shape; + bool reversed{false}; +}; + +double constexpr tolerance{0.02}; + +void reset(); +void load(); +std::vector const &records(); +std::optional at(glm::dvec3 const &Point, double const Reach = tolerance); +// the track lies along a stored alignment, its middle is the hit +std::optional on_track(TTrack const &Track, double const Reach = tolerance); +// the vertex the tracks follow, if they all lie along one stored alignment and the curve elements under them belong to one vertex +std::optional vertex_of(std::vector> const &Run); +// stored alignment with these ends, either way +int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed); +// stored alignment near the tracks, whose line they no longer follow; Departure receives the largest distance +int departed(std::vector const &Tracks, double &Departure); +// keeps the design in the file of the layer, in place of the one with the same ends +int store(alignment::design const &Design, alignment::result const &Result, double const Origin, scene::layer_handle const Layer); +void forget(int const Index); +// km of the point on the stored alignments, if it lies on one +std::optional chainage_at(glm::dvec3 const &Point); + +} // namespace alignment_store diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 99d3236e..24b3d79d 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -11,6 +11,7 @@ http: #include "editor/editorTrack.hpp" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorAlignmentStore.hpp" #include "world/Track.h" #include "vehicle/DynObj.h" @@ -1756,6 +1757,26 @@ bool editor_track::analyse_curve(std::vector> const &R Curve.arcs = arcs; } } + if (Curve.reversals == 0) + { + if (auto const designed{alignment_store::vertex_of(Run)}) + { + auto const &shape{designed->shape}; + Curve.designed = shape; + Curve.radius = shape.radius; + Curve.transition_in = shape.transition_in; + Curve.transition_out = shape.transition_out; + Curve.cant = shape.cant; + Curve.compound = shape.compound && false == shape.arcs.empty(); + Curve.arcs.clear(); + if (Curve.compound) + { + Curve.arcs.push_back({shape.radius, shape.share, 0.0}); + for (auto const &arc : shape.arcs) + Curve.arcs.push_back({arc.radius, arc.share, arc.transition}); + } + } + } Curve.from = run.front().first; Curve.to = run.back().first; return true; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 8f8040aa..995219d5 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -11,12 +11,14 @@ http: #include #include +#include #include #include #include #include #include "world/Segment.h" +#include "editor/editorAlignment.hpp" #include "utilities/Classes.h" class TTrack; @@ -212,6 +214,8 @@ class editor_track double transition{0.0}; }; std::vector arcs; + // the parameters of the vertex in the alignment kept in the scenery, in the direction of the run, when the paths follow one + std::optional designed; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); // curve made of the given paths, each with the flag of being run from its start diff --git a/lang/editor_pl.po b/lang/editor_pl.po index caade69d..85e289ab 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5313,3 +5313,36 @@ msgstr "pochylenie zmienione o %+.1f‰" msgid "cant %.0f mm (%+.0f mm)" msgstr "przechyłka %.0f mm (%+.0f mm)" + +msgid "\nCurves read from the design kept in the scenery: %d" +msgstr "\nŁuki odczytane z projektu zapisanego w scenerii: %d" + +msgid "\nCurves recognized from the paths, the scenery keeps no design for them" +msgstr "\nŁuki rozpoznane z odcinków, sceneria nie ma dla nich zapisanego projektu" + +msgid "Km of the start" +msgstr "Km początku" + +msgid "Chainage of the start of the fragment, kept with the design in the scenery; the vertical profile of these paths counts from it" +msgstr "Kilometraż początku fragmentu, zapisywany z projektem w scenerii; profil podłużny tych odcinków liczy się od niego" + +msgid "The design kept in %s for this fragment departs from its paths by up to %.2f m" +msgstr "Projekt zapisany w %s dla tego fragmentu odbiega od jego odcinków o maks. %.2f m" + +msgid "Take the kept design" +msgstr "Weź zapisany projekt" + +msgid "The kept design is loaded, applying it lays the paths by it again" +msgstr "Wczytano zapisany projekt, zastosowanie ułoży odcinki ponownie według niego" + +msgid "Loads the design kept in the scenery in place of the one read from the paths; nothing changes until it's applied" +msgstr "Wczytuje projekt zapisany w scenerii w miejsce odczytanego z odcinków; nic się nie zmienia do zastosowania" + +msgid "Forget the kept design" +msgstr "Zapomnij zapisany projekt" + +msgid "The kept design will be removed from " +msgstr "Zapisany projekt zostanie usunięty z " + +msgid "The paths stay as they are, the scenery no longer keeps a design for them" +msgstr "Odcinki zostają bez zmian, sceneria przestaje przechowywać dla nich projekt" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index b735e344..aef7ac05 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -837,7 +837,7 @@ bool node_layers::pending() const namespace { // marks of the lines the editor keeps track of -std::array const editormarks{"//$p", "//$b", "//$c"}; +std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "//$c"}; } // namespace void node_layers::scan(layer_handle const Layer) From 0bfb8a4e7c522c04b65b728ce3d4cda7455ecd2b Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 16:33:25 +0200 Subject: [PATCH 016/150] feat(editor): switches kept in the scenery as //$r lines: type (Rz, arc Rlj/Rld, slip Rkpd), rail, R, 1:n, side, a, b, R of the main track, the parts of a slip, km and number, catalogue or measured; recorded as they are placed on a straight, in a curve (one-sided when diverging to the side the curve turns to), as a double slip, and when their geometry is replaced or flipped; the switch panel shows the kept type and the R of the diverging track bent with the main one, with the number, rail and km to edit (km from the kept horizontal alignment); the speed check of a diverging track takes its R from the kept type; records follow renamed switches and go with removed ones on save; editor records stay out of the terrain files (.txtf), and those written there by hand go with them into .btf; the side of the diverging track shown as seen; self-test commands for placing switches, the kept switches and the .btf conversion; Polish translation --- CMakeLists.txt | 1 + application/editormode.cpp | 3 + application/editormode.h | 3 + application/editormodeselftest.cpp | 112 +++++++++++++ application/editormodetrack.cpp | 165 +++++++++++++++++- editor/editorSpeed.cpp | 7 +- editor/editorSwitchStore.cpp | 257 +++++++++++++++++++++++++++++ editor/editorSwitchStore.hpp | 61 +++++++ editor/editorTrack.cpp | 2 + editor/editorTrack.hpp | 2 + lang/editor_pl.po | 30 ++++ 11 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 editor/editorSwitchStore.cpp create mode 100644 editor/editorSwitchStore.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d6b7d24..4c023ec6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,7 @@ set(SOURCES "editor/editorRoad.cpp" "editor/editorAlignment.cpp" "editor/editorAlignmentStore.cpp" +"editor/editorSwitchStore.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 134c5335..bd3000a2 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -29,6 +29,7 @@ extern char **environ; #include "editor/editorGroundMesh.hpp" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIcons.hpp" #include "utilities/translation.h" @@ -497,6 +498,7 @@ void editor_mode::editormode_input::poll() editor_mode::editor_mode() { alignment_store::reset(); + switch_store::reset(); m_userinterface = std::make_shared(); // the area fill settings live in the node bank window, in the tab of the fill mode ui()->set_fill_options([this]() { render_area_fill(); }); @@ -2904,6 +2906,7 @@ bool editor_mode::save() if (m_profile.changed) profile_store(); + switch_store::tidy(); infra_store(); if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) { diff --git a/application/editormode.h b/application/editormode.h index 015497bd..f5b4fa53 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -787,6 +787,9 @@ class editor_mode : public application_mode, private editor_track::observer bool route_load(TTrack &Track); void route_read_stored(); void route_take_stored(int const Index); + void record_switch(TTrack &Track, editor_track::switch_template const &Shape); + void record_slip(std::vector const &Parts, std::string const &Kind, std::string const &Type, double const Radius, double const Angle, std::string const &Base); + void render_switch_record(TTrack &Track); void route_store(); void render_route_stored(); void route_update() { m_route.result = alignment::compute(m_route.design); } diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 50558c8c..dd6622b7 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -34,6 +34,9 @@ http://mozilla.org/MPL/2.0/. #include #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "scene/sceneterrain.h" +#include "editor/editorSpeed.hpp" #include #include @@ -98,6 +101,10 @@ http://mozilla.org/MPL/2.0/. // routeapply, routetake as the buttons of the route design do it // alignments logs the alignments kept in the scenery and the element under each path // splitkind , nudgekind the first path over an element of the kind split in two, or moved +// switchplace