From bf24c3983a4def01561e49caf4d8cc87ac472636 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sat, 3 Oct 2026 00:49:35 +0200 Subject: [PATCH] feat(editor): no sub-metre straights, switches snap, K splits path --- application/editormode.cpp | 14 +++++ application/editormodetrack.cpp | 33 +++++++++++- editor/editorAlignment.cpp | 90 +++++++++++++++++++++++++++++---- editor/editorAlignment.hpp | 1 + editor/editorTrack.cpp | 71 ++++++++++++++++++++++++++ editor/editorTrack.hpp | 2 + 6 files changed, 200 insertions(+), 11 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index ff40cc26..dd8bafa2 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -2768,6 +2768,20 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in m_fill_points.pop_back(); break; + case GLFW_KEY_K: + if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && selected_track() != nullptr) + { + auto *track{selected_track()}; + auto const parameter{editor_track::nearest_parameter(*track, Global.pCamera.Pos + GfxRenderer->Mouse_Position())}; + auto const before{editor_track::capture(*track)}; + if (auto *created{editor_track::split_path(*track, parameter)}) + { + push_track_snapshot({{track, before}}, {created}); + straight_refresh(); + } + } + break; + case GLFW_KEY_F: if (is_press(Action)) { diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4f727c40..98e08fbc 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -836,6 +836,7 @@ void editor_mode::route_reset() alignment::design design; 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}); }; @@ -1221,6 +1222,11 @@ void editor_mode::render_route_ui() design.shape = static_cast(shape); changed = true; } + if (ImGui::SliderInt("Paths per transition curve", &design.transition_pieces, 1, 4)) + { + route_update(); + route_apply(); + } ImGui::PopItemWidth(); if (ImGui::TreeNode("Limits")) @@ -2355,7 +2361,7 @@ void editor_mode::draw_track_hints() hint = m_route.chain.tracks.empty() ? "LMB: select a curve" : "Drag the vertex (yellow) or the radius grip (green), the change is applied on release Ctrl+Z: undo"; else hint = "LMB: select a path or a point handle"; - hint += " O: top view"; + hint += " K: split the path under the cursor O: top view"; ImGuiIO const &io = ImGui::GetIO(); auto *drawlist{ImGui::GetBackgroundDrawList()}; ImVec2 const position{12.0f, io.DisplaySize.y - 28.0f}; @@ -2542,7 +2548,30 @@ void editor_mode::insert_switch(editor_track::straight const &Line, double const auto const &shape{m_switch.templates[m_switch.armed]}; if (shape.length > Line.length - 0.02) return; - auto const from{std::clamp(Direction > 0 ? Along : Along - shape.length, 0.01, Line.length - shape.length - 0.01)}; + auto from{std::clamp(Direction > 0 ? Along : Along - shape.length, 0.0, Line.length - shape.length)}; + { + std::vector joints{0.0, Line.length}; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + joints.push_back(glm::dot(glm::dvec2{path.points[index].x - Line.start.x, path.points[index].z - Line.start.z}, Line.direction)); + } + auto const nearest = [&](double const Value) { + double best{Value}; + for (auto const joint : joints) + if (std::abs(joint - Value) < std::abs(best - Value) || best == Value) + best = std::abs(joint - Value) < 1.0 ? joint : best; + return best; + }; + auto const snappedstart{nearest(from)}; + auto const snappedend{nearest(from + shape.length)}; + if (snappedstart != from) + from = snappedstart; + else if (snappedend != from + shape.length) + from = snappedend - shape.length; + from = std::clamp(from, 0.0, Line.length - shape.length); + } auto const to{from + shape.length}; auto const origin{Direction > 0 ? from : to}; struct span diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index 39df9129..5f41b0b7 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -217,7 +217,7 @@ glm::dvec2 blossom(std::array const &Points, double const A, doub return glm::mix(s[0], s[1], C); } -std::size_t element_pieces(element const &Element, transition_shape const Shape) +std::size_t element_pieces(element const &Element, transition_shape const Shape, int const Transitions) { if (Element.length < 1e-3) return 0; @@ -227,11 +227,12 @@ std::size_t element_pieces(element const &Element, transition_shape const Shape) return static_cast(std::max(1.0, std::ceil(Element.length / Element.radius / (10.0 * kPi / 180.0) - 1e-9))); case element_kind::transition_in: case element_kind::transition_out: - return Shape == transition_shape::cubic_parabola ? 1 : static_cast(std::max(1.0, std::ceil(Element.length / 20.0 - 1e-9))); + return std::max(std::max(1, Transitions), Shape == transition_shape::cubic_parabola ? 1 : static_cast(std::max(1.0, std::ceil(Element.length / 20.0 - 1e-9)))); case element_kind::spiral: { auto const turn{(Element.curvature_start + Element.curvature_end) * 0.5 * Element.length}; - return static_cast(std::max({1.0, std::ceil(turn / (10.0 * kPi / 180.0) - 1e-9), std::ceil(Element.length / 20.0 - 1e-9)})); + auto const count{static_cast(std::max({1.0, std::ceil(turn / (10.0 * kPi / 180.0) - 1e-9), std::ceil(Element.length / 20.0 - 1e-9)}))}; + return Element.curvature_start != Element.curvature_end ? std::max(count, std::max(1, Transitions)) : count; } default: return 1; @@ -854,7 +855,7 @@ std::size_t minimum_pieces(result const &Result, design const &Design) { std::size_t count{0}; for (auto const &e : Result.elements) - count += element_pieces(e, Design.shape); + count += element_pieces(e, Design.shape, Design.transition_pieces); return count; } @@ -864,27 +865,98 @@ std::vector pieces(result const &Result, design const &Design, std { std::size_t element; double from, to; + bool generic{false}; + double start{0.0}; + double end{0.0}; }; + auto const extent = [&](span const &Span) { return Span.generic ? Span.end - Span.start : Span.to - Span.from; }; + auto const chainage_from = [&](span const &Span) { return Span.generic ? Span.start : Result.elements[Span.element].chainage + Span.from; }; + auto const chainage_to = [&](span const &Span) { return Span.generic ? Span.end : Result.elements[Span.element].chainage + Span.to; }; std::vector spans; for (std::size_t i = 0; i < Result.elements.size(); ++i) { auto const &e{Result.elements[i]}; - auto const count{element_pieces(e, Design.shape)}; + auto const count{element_pieces(e, Design.shape, Design.transition_pieces)}; for (std::size_t j = 0; j < count; ++j) spans.push_back({i, e.length * j / count, e.length * (j + 1) / count}); } + double const shortest{1.0}; + for (std::size_t i = 0; i < spans.size() && spans.size() > 1;) + { + auto const &e{Result.elements[spans[i].element]}; + if (e.kind != element_kind::straight || extent(spans[i]) >= shortest) + { + ++i; + continue; + } + auto const start{chainage_from(spans[i])}; + auto const end{chainage_to(spans[i])}; + if (i > 0) + { + auto &previous{spans[i - 1]}; + previous.start = chainage_from(previous); + previous.end = end; + previous.generic = true; + } + else + { + auto &next{spans[i + 1]}; + next.end = chainage_to(next); + next.start = start; + next.generic = true; + } + spans.erase(spans.begin() + i); + } while (false == spans.empty() && spans.size() < Count) { - auto longest = std::max_element(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.to - A.from < B.to - B.from; }); - auto const middle{(longest->from + longest->to) * 0.5}; - span const second{longest->element, middle, longest->to}; - longest->to = middle; + auto longest = std::max_element(spans.begin(), spans.end(), [&](span const &A, span const &B) { return extent(A) < extent(B); }); + span second{*longest}; + if (longest->generic) + { + auto const middle{(longest->start + longest->end) * 0.5}; + second.start = middle; + longest->end = middle; + } + else + { + auto const middle{(longest->from + longest->to) * 0.5}; + second.from = middle; + longest->to = middle; + } spans.insert(std::next(longest), second); } std::vector result; for (auto const &piece : spans) { + if (piece.generic) + { + auto const first{evaluate(Result, piece.start)}; + auto const last{evaluate(Result, piece.end)}; + glm::dvec2 const p0{first.position.x, first.position.z}; + glm::dvec2 const p3{last.position.x, last.position.z}; + auto const d0{glm::normalize(glm::dvec2{first.direction.x, first.direction.z})}; + auto const d3{glm::normalize(glm::dvec2{last.direction.x, last.direction.z})}; + auto const handle{glm::distance(p0, p3) / 3.0}; + auto const p1{p0 + d0 * handle}; + auto const p2{p3 - d3 * handle}; + auto const y0{first.position.y}; + auto const y3{last.position.y}; + auto const y1{y0 + grade(Result, piece.start) * handle}; + auto const y2{y3 - grade(Result, piece.end) * handle}; + auto const roll = [&](station const &Station) { return static_cast(Station.turn * glm::degrees(std::asin(std::clamp(Station.cant / Design.norms.gauge, -1.0, 1.0)))); }; + segment_data path; + path.points[segment_data::point::start] = first.position; + path.points[segment_data::point::end] = last.position; + path.points[segment_data::point::control1] = glm::dvec3{p1.x, y1, p1.y} - first.position; + path.points[segment_data::point::control2] = glm::dvec3{p2.x, y2, p2.y} - last.position; + path.rolls[0] = roll(first); + path.rolls[1] = roll(last); + auto const &e{Result.elements[piece.element]}; + path.radius = e.kind == element_kind::straight ? 0.f : static_cast(e.radius); + result.push_back(path); + continue; + } auto const &e{Result.elements[piece.element]}; glm::dvec2 p0, p3, d0, d3; element_point(e, Design.shape, piece.from, p0, d0); diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 51c597a4..13984180 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -68,6 +68,7 @@ struct design std::vector vertices; transition_shape shape{transition_shape::cubic_parabola}; double speed{100.0}; + int transition_pieces{1}; limits norms; }; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index bc6fddc3..d6e80d05 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1500,6 +1500,77 @@ TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) return track; } +double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Point) +{ + if (Track.m_paths.empty()) + return 0.5; + auto const &path{Track.m_paths.front()}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const at = [&](double const T) { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + }; + auto const distance = [&](double const T) { auto const q{at(T)}; return glm::distance(glm::dvec2{q.x, q.z}, glm::dvec2{Point.x, Point.z}); }; + double best{0.5}; + for (int i = 0; i <= 200; ++i) + if (distance(i / 200.0) < distance(best)) + best = i / 200.0; + double step{1.0 / 200.0}; + for (int i = 0; i < 30; ++i) + { + step *= 0.5; + if (best - step > 0.0 && distance(best - step) < distance(best)) + best -= step; + else if (best + step < 1.0 && distance(best + step) < distance(best)) + best += step; + } + return best; +} + +TTrack *editor_track::split_path(TTrack &Track, double const T) +{ + if (Track.eType != tt_Normal || Track.m_paths.empty() || T <= 0.001 || T >= 0.999) + return nullptr; + auto const path{Track.m_paths.front()}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + segment_data first{path}; + segment_data second{path}; + auto const roll{static_cast(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)}; + if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + { + auto const middle{glm::mix(p0, p3, T)}; + first.points[segment_data::point::end] = middle; + second.points[segment_data::point::start] = middle; + } + else + { + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const p01{glm::mix(p0, p1, T)}; + auto const p12{glm::mix(p1, p2, T)}; + auto const p23{glm::mix(p2, p3, T)}; + auto const p012{glm::mix(p01, p12, T)}; + auto const p123{glm::mix(p12, p23, T)}; + auto const middle{glm::mix(p012, p123, T)}; + first.points[segment_data::point::control1] = p01 - p0; + first.points[segment_data::point::control2] = p012 - middle; + first.points[segment_data::point::end] = middle; + second.points[segment_data::point::start] = middle; + second.points[segment_data::point::control1] = p123 - middle; + second.points[segment_data::point::control2] = p23 - p3; + } + first.rolls[1] = roll; + second.rolls[0] = roll; + auto *created{create_path(Track, second)}; + Track.m_paths.front() = first; + commit({&Track, created}); + return created; +} + TTrack *editor_track::load_path(std::string const &Text, TTrack const &Template) { cParser parser(Text, cParser::buffer_TEXT); diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index d96358d4..f1b535a1 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -150,6 +150,8 @@ class editor_track static std::vector find_switch_templates(); static TTrack *load_path(std::string const &Text, TTrack const &Template); static TTrack *create_path(TTrack const &Style, segment_data const &Path); + static TTrack *split_path(TTrack &Track, double const T); + static double nearest_parameter(TTrack const &Track, glm::dvec3 const &Point); static std::vector place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade); static TTrack *create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style); static void move_straights(std::vector const &Lines, std::vector> const &Ends);