From cdd5c242d1ccddf6b521abdb83f978be43220801 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 00:14:03 +0200 Subject: [PATCH] feat(editor): curves take over the adjoining straights --- application/editormode.h | 1 + application/editormodetrack.cpp | 112 ++++++++++++++++++++++++------ editor/editorAlignment.cpp | 89 ++++++++++++++++++------ editor/editorAlignment.hpp | 7 ++ editor/editorTrack.cpp | 116 ++++++++++++++++++++++++++++++++ editor/editorTrack.hpp | 10 +++ 6 files changed, 297 insertions(+), 38 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 262b1b1b..19438feb 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -313,6 +313,7 @@ class editor_mode : public application_mode }; void render_route_ui(); void route_reset(); + void route_bind_ends(); bool route_from_curve(TTrack &Track); void route_update() { m_route.result = alignment::compute(m_route.design); } void route_apply(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index e8adfae1..8ebd64f9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -433,7 +433,17 @@ void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::ve if (false == m_route.chain.tracks.empty()) { std::string error; - editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error); + if (m_route.from->m_editorremoved || m_route.to->m_editorremoved || false == editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error)) + { + m_route.chain = {}; + m_route.result = {}; + m_route.vertex = m_route.grip = -1; + } + else + { + route_bind_ends(); + route_update(); + } } m_track_snap = {}; @@ -818,6 +828,24 @@ void editor_mode::route_recommend(alignment::vertex &Vertex) const Vertex.vertical_radius = 0.0; } +void editor_mode::route_bind_ends() +{ + auto const &chain{m_route.chain}; + auto &design{m_route.design}; + auto const plan = [](glm::dvec3 const &Direction) { return glm::normalize(glm::dvec2{Direction.x, Direction.z}); }; + auto const slope = [](glm::dvec3 const &Direction) { return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); }; + design.start = chain.start; + design.end = chain.end; + design.start_direction = plan(chain.start_direction); + design.end_direction = plan(chain.end_direction); + design.start_grade = slope(chain.start_direction); + design.end_grade = slope(chain.end_direction); + design.start_radius = chain.start_radius; + design.end_radius = chain.end_radius; + design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); + design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); +} + void editor_mode::route_reset() { auto &route{m_route}; @@ -837,18 +865,10 @@ void editor_mode::route_reset() design.norms = route.design.norms; design.shape = route.design.shape; design.transition_pieces = route.design.transition_pieces; - design.start = chain.start; - design.end = chain.end; - auto const plan = [](glm::dvec3 const &Direction) { return glm::normalize(glm::dvec2{Direction.x, Direction.z}); }; - auto const slope = [](glm::dvec3 const &Direction) { return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); }; - design.start_direction = plan(chain.start_direction); - design.end_direction = plan(chain.end_direction); - design.start_grade = slope(chain.start_direction); - design.end_grade = slope(chain.end_direction); - design.start_radius = chain.start_radius; - design.end_radius = chain.end_radius; design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; route.design = design; + route_bind_ends(); + design = route.design; glm::dvec2 intersection; if (false == alignment::collinear(design)) @@ -938,16 +958,64 @@ void editor_mode::route_apply() } } - auto const pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; std::vector> states; + std::vector created; + auto *from{route.from}; + auto *to{route.to}; + auto const startextension{route.result.start_extension}; + auto const endextension{route.result.end_extension}; + if (startextension > 0.0 || endextension > 0.0) + { + auto const before{chain}; + if (startextension > 0.0) + from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created); + if (endextension > 0.0) + to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); + auto const plandistance = [](glm::dvec3 const &A, glm::dvec3 const &B) { return glm::distance(glm::dvec2{A.x, A.z}, glm::dvec2{B.x, B.z}); }; + std::string error; + if (false == editor_track::find_chain(from, to, chain, error) || plandistance(chain.start, route.result.start) > 5.0 || plandistance(chain.end, route.result.end) > 5.0) + { + for (auto *track : created) + editor_track::retire(*track); + for (auto entry = states.rbegin(); entry != states.rend(); ++entry) + { + editor_track::apply(*entry->first, entry->second); + editor_track::commit({entry->first}); + } + route.error = error.empty() ? "The adjoining straights can't be cut at the ends of the curve" : error; + return; + } + } for (auto *track : chain.tracks) - states.emplace_back(track, editor_track::capture(*track)); - auto created{editor_track::relay(chain, pieces)}; - auto const added{created.size()}; + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + auto relaid{editor_track::relay(chain, pieces)}; + auto const added{relaid.size()}; + created.insert(created.end(), relaid.begin(), relaid.end()); push_track_snapshot(std::move(states), std::move(created)); + route.from = from; + route.to = to; editor_track::find_chain(route.from, route.to, route.chain, route.error); + if (startextension > 0.0 || endextension > 0.0) + { + route_bind_ends(); + auto &vertices{route.design.vertices}; + if (vertices.size() >= 2) + { + vertices.front().offset += startextension; + vertices.back().offset += endextension; + } + route_update(); + } route.status = "Re-laid " + std::to_string(chain.tracks.size()) + " paths" + (added > 0 ? ", added " + std::to_string(added) : std::string{}); + if (startextension > 0.0 || endextension > 0.0) + { + char text[96]; + std::snprintf(text, sizeof(text), ", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); + route.status += text; + } WriteLog("Editor: route design - " + route.status, logtype::generic); } @@ -1033,11 +1101,13 @@ void editor_mode::draw_route_overlay() const auto const &result{route.result}; screen_projection const projection; ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + auto const &start{result.valid ? result.start : design.start}; + auto const &end{result.valid ? result.end : design.end}; - std::vector polygon{design.start}; + std::vector polygon{start}; for (int i = 0; i < static_cast(design.vertices.size()); ++i) polygon.push_back(route_vertex_position(i)); - polygon.push_back(design.end); + polygon.push_back(end); for (std::size_t i = 0; i + 1 < polygon.size(); ++i) projection.line(drawlist, polygon[i], polygon[i + 1], IM_COL32(255, 210, 60, 150), 1.5f); @@ -1062,7 +1132,7 @@ void editor_mode::draw_route_overlay() const for (auto const atend : {false, true}) { - auto const &point{atend ? design.end : design.start}; + auto const &point{atend ? end : start}; auto const direction{atend ? design.end_direction : design.start_direction}; ImVec2 screen; if (projection.project(point, screen)) @@ -1369,7 +1439,10 @@ void editor_mode::render_route_ui() auto const pieces{alignment::minimum_pieces(result, design)}; ImGui::Text("Length %.2f m (now %.2f m), %zu elements", result.length, route.chain.length, result.elements.size()); ImGui::TextDisabled("Paths: %zu in the fragment, %zu needed%s", route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? " (copies of the neighbours will be added)" : ""); + if (result.start_extension > 0.0 || result.end_extension > 0.0) + ImGui::Text("Takes over %.2f m of the straight at the start, %.2f m at the end", result.start_extension, result.end_extension); } + ImGui::TextDisabled("Straight available beyond the ends: %.2f / %.2f m", design.start_reserve, design.end_reserve); for (auto const &error : result.errors) ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str()); for (auto const &warning : result.warnings) @@ -2165,6 +2238,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co state.status = m_route.error; return; } + m_route.design.start_reserve = m_route.design.end_reserve = 0.0; for (auto &vertex : m_route.design.vertices) { vertex.radius = Radius; @@ -2185,7 +2259,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co state.status = m_route.result.errors.empty() ? "The curve can't be fitted" : m_route.result.errors.front(); return; } - auto const pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; auto created{editor_track::relay(m_route.chain, pieces)}; editor_track::commit(tracks); for (auto const &entry : splitstates) diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 703683ce..b93046b9 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -277,11 +277,18 @@ recommendation recommend(double const Speed, double const Radius, limits const & return result; } -result compute(design const &Design) +namespace +{ + +result fit_between(design const &Design) { result r; r.shape = Design.shape; r.gauge = Design.norms.gauge; + r.start = Design.start; + r.end = Design.end; + auto const startreserve{std::max(0.0, Design.start_reserve)}; + auto const endreserve{std::max(0.0, Design.end_reserve)}; auto const start{plan(Design.start)}; auto const end{plan(Design.end)}; @@ -351,6 +358,16 @@ result compute(design const &Design) straight.length = Length; push(straight); }; + auto const starts_early = [&](double const Straight) { + if (Straight < 0.0) + r.start_extension = -Straight; + if (Straight >= -1e-4 || -Straight <= startreserve + 1e-4) + return; + if (startreserve > 0.0) + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the adjoining straight", -Straight - startreserve)); + else + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -Straight)); + }; struct fitted { @@ -441,9 +458,9 @@ result compute(design const &Design) std::vector scales(count, 1.0); for (std::size_t leg = 0; leg + 1 < polygon.size(); ++leg) { - auto const available{glm::distance(polygon[leg], polygon[leg + 1])}; + auto const available{glm::distance(polygon[leg], polygon[leg + 1]) + (leg == 0 ? startreserve : 0.0) + (leg == count ? endreserve : 0.0)}; auto const required{(leg > 0 ? fits[leg - 1].tangent_out : 0.0) + (leg < count ? fits[leg].tangent_in : 0.0)}; - if (required <= available) + if (required <= available + 1e-6) continue; auto const scale{available / required * (1.0 - 1e-9)}; if (leg > 0) @@ -534,13 +551,10 @@ result compute(design const &Design) auto const along{cross(directionout, endoffset) / cross(directionout, directionin)}; auto const curvestart{position - directionin * along}; auto const straight{glm::dot(curvestart - cursor, directionin)}; - if (straight < -1e-4) - { - if (k == 0) - r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight)); - else - r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); - } + if (k == 0) + starts_early(straight); + else if (straight < -1e-4) + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); push_straight(cursor, directionin, straight); auto const startchainage{chainage}; glm::dvec2 origin{curvestart}; @@ -615,13 +629,10 @@ result compute(design const &Design) } auto const straight{glm::dot(curvestart - cursor, directionin)}; - if (straight < -1e-4) - { - if (k == 0) - r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -straight)); - else - r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); - } + if (k == 0) + starts_early(straight); + else if (straight < -1e-4) + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); push_straight(cursor, directionin, straight); element curve; @@ -698,8 +709,15 @@ result compute(design const &Design) } auto const lastdirection{glm::normalize(polygon.back() - polygon[polygon.size() - 2])}; auto const tail{glm::dot(end - cursor, lastdirection)}; - if (tail < -1e-4) - r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + if (tail < 0.0) + r.end_extension = -tail; + if (tail < -1e-4 && -tail > endreserve + 1e-4) + { + if (endreserve > 0.0) + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the adjoining straight", count, -tail - endreserve)); + else + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + } push_straight(cursor, lastdirection, tail); r.length = chainage; @@ -757,6 +775,39 @@ result compute(design const &Design) return r; } +glm::dvec3 along_end(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Distance) +{ + auto const direction{glm::normalize(Direction)}; + return {Point.x + direction.x * Distance, Point.y + Grade * Distance, Point.z + direction.y * Distance}; +} + +} // namespace + +result compute(design const &Design) +{ + auto first{fit_between(Design)}; + auto const startextension{first.start_extension > 1e-4 ? first.start_extension : 0.0}; + auto const endextension{first.end_extension > 1e-4 ? first.end_extension : 0.0}; + if (false == first.errors.empty() || (startextension == 0.0 && endextension == 0.0)) + { + first.start_extension = first.end_extension = 0.0; + return first; + } + auto extended{Design}; + extended.start = along_end(Design.start, Design.start_direction, Design.start_grade, -startextension); + extended.end = along_end(Design.end, Design.end_direction, Design.end_grade, endextension); + extended.start_reserve = extended.end_reserve = 0.0; + if (extended.vertices.size() >= 2) + { + extended.vertices.front().offset += startextension; + extended.vertices.back().offset += endextension; + } + auto r{fit_between(extended)}; + r.start_extension = startextension; + r.end_extension = endextension; + return r; +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { auto const start{plan(Design.start)}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index f8628170..a3e4572f 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -65,6 +65,9 @@ struct design double end_grade{0.0}; double start_radius{0.0}; double end_radius{0.0}; + // length of the straight beyond the start and the end, which the curves can take over + double start_reserve{0.0}; + double end_reserve{0.0}; std::vector vertices; transition_shape shape{transition_shape::cubic_parabola}; double speed{100.0}; @@ -152,6 +155,10 @@ struct result std::vector curves; std::vector profile; double length{0.0}; + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + double start_extension{0.0}; + double end_extension{0.0}; }; result compute(design const &Design); diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 73ebb580..6df8d41b 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -27,6 +27,7 @@ http: #include #include #include +#include #include namespace @@ -1179,6 +1180,121 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance return true; } +namespace +{ + +// shorter remainders of a straight path are absorbed by moving the joint instead of splitting the path +double split_margin(double const Length) +{ + return std::max(0.5, 0.002 * Length); +} + +double plan_length(segment_data const &Path) +{ + return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])); +} + +} // namespace + +editor_track::straight_run editor_track::straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance) +{ + straight_run run; + if (Chain.tracks.empty()) + return run; + auto *edge{Atend ? Chain.tracks.back() : Chain.tracks.front()}; + bool const forward{Atend ? Chain.forward.back() : Chain.forward.front()}; + TTrack *current{forward == Atend ? edge->trNext : edge->trPrev}; + auto joint{Atend ? Chain.end : Chain.start}; + auto const origin{plan_of(joint)}; + auto direction{glm::normalize(plan_of(Atend ? Chain.end_direction : -Chain.start_direction))}; + while (current != nullptr && run.tracks.size() < 1000) + { + std::string reason; + if (current->eType != tt_Normal || current->m_editorremoved || false == can_edit_geometry(*current, reason)) + break; + if (std::find(Chain.tracks.begin(), Chain.tracks.end(), current) != Chain.tracks.end() || std::find(run.tracks.begin(), run.tracks.end(), current) != run.tracks.end()) + break; + auto const &path{current->m_paths.front()}; + if (false == plan_straight(path, Tolerance)) + break; + bool outward; + if (glm::distance(path.points[segment_data::point::start], joint) <= kSamePoint) + outward = true; + else if (glm::distance(path.points[segment_data::point::end], joint) <= kSamePoint) + outward = false; + else + break; + auto const &far{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + if (run.tracks.empty()) + { + auto const own{glm::normalize(plan_of(far - joint))}; + if (std::abs(lateral_of(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0) + break; + direction = own; + } + auto const offset{plan_of(far) - origin}; + auto const along{glm::dot(offset, direction)}; + if (along <= run.length + 1e-3 || std::abs(lateral_of(direction, offset)) > Tolerance.offset) + break; + run.tracks.push_back(current); + run.outward.push_back(outward); + run.length = along; + joint = far; + current = outward ? current->trNext : current->trPrev; + } + return run; +} + +double editor_track::run_reserve(straight_run const &Run) +{ + if (Run.tracks.empty()) + return 0.0; + return std::max(0.0, Run.length - split_margin(plan_length(Run.tracks.back()->m_paths.front())) - 0.01); +} + +TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created) +{ + auto const length{glm::length(plan_of(Cut - Joint))}; + auto *edge{Edge}; + double covered{0.0}; + for (std::size_t i = 0; i < Run.tracks.size(); ++i) + { + auto *track{Run.tracks[i]}; + auto const outward{Run.outward[i]}; + auto const &path{track->m_paths.front()}; + auto const pathlength{plan_length(path)}; + auto const margin{split_margin(pathlength)}; + auto const remaining{length - covered}; + if (remaining <= margin) + { + if (std::abs(remaining) > 1e-4) + { + States.emplace_back(track, capture(*track)); + move_point(*track, {0, outward ? point_kind::start : point_kind::end}, Cut); + commit({track}); + } + break; + } + if (remaining < pathlength - margin) + { + auto const &nearpoint{path.points[outward ? segment_data::point::start : segment_data::point::end]}; + auto const &farpoint{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + auto const t{nearest_parameter(*track, nearpoint + (farpoint - nearpoint) * (remaining / pathlength))}; + auto const before{capture(*track)}; + auto *created{split_path(*track, t)}; + if (created == nullptr) + break; + States.emplace_back(track, before); + Created.push_back(created); + edge = outward ? track : created; + break; + } + edge = track; + covered += pathlength; + } + return edge; +} + std::vector editor_track::find_switch_templates() { std::vector result; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 45035212..13800f43 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -137,6 +137,16 @@ class editor_track double split{0.5}; }; static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); + // regular straight paths continuing a chain end in line, which the chain can take over + struct straight_run + { + std::vector tracks; + std::vector outward; + double length{0.0}; + }; + static straight_run straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance); + static double run_reserve(straight_run const &Run); + static TTrack *take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created); struct switch_template { TTrack *source{nullptr};