From 4ea2f4f13fa694a8a200faf383c62a7b6740958a Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 11 Oct 2026 16:57:23 +0200 Subject: [PATCH] feat(editor): the kept alignments and switches after a drag of the gizmo or of a point: a line all of whose paths are moved or turned as a whole takes its kept design along; paths pulled out of a kept design, or given another cant, are told about on the status line with the way back; a switch moved or turned as a whole keeps its type and its record follows it, with its km; a switch bent out of the shape of its type loses it and is told about (//$r records keep the shape of the switch); all of it in the same undo step as the drag; Polish translation --- application/editormode.h | 12 +++ application/editormodetrack.cpp | 144 ++++++++++++++++++++++++++++++ application/editormodetrackux.cpp | 2 + editor/editorAlignmentStore.cpp | 43 +++++++++ editor/editorAlignmentStore.hpp | 6 ++ editor/editorSwitchStore.cpp | 88 ++++++++++++++++-- editor/editorSwitchStore.hpp | 10 ++- lang/editor_pl.po | 12 +++ 8 files changed, 307 insertions(+), 10 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 1c643f32..f679db2b 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -36,6 +36,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -636,6 +637,17 @@ class editor_mode : public application_mode, private editor_track::observer void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); // changes of the kept alignments and switches go into the last step of the history, made just before them void records_to_history(); + // the paths a drag of the gizmo takes, to tell after it what became of the kept alignments and switches + struct record_watch + { + bool active{false}; + std::vector moving; + std::vector> before; // ends of the first path of each moving track + std::map> on; // the kept alignments and the moving paths on them + std::set whole; // the kept alignments all of whose paths move + } m_record_watch; + void records_watch_begin(); + void records_watch_end(); editor_track::point_ref m_track_point; std::vector m_track_drag; std::vector> m_track_drag_points; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index be970a92..1057c809 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -917,6 +917,7 @@ void editor_mode::track_gizmo_begin(TTrack &Track, bool const Pointmode, gizmo_o for (auto *dragged : m_track_drag) states.emplace_back(dragged, editor_track::capture(*dragged)); push_track_snapshot(std::move(states)); + records_watch_begin(); track_gizmo_hold(); for (auto &link : drag.switches) for (auto &member : link.cluster) @@ -1097,6 +1098,7 @@ void editor_mode::track_gizmo_end(TTrack &Track) created.insert(created.end(), m_track_gizmo_drag.extra.begin(), m_track_gizmo_drag.extra.end()); m_track_gizmo_drag.extra.clear(); } + records_watch_end(); m_track_gizmo_using = false; m_track_drag.clear(); m_track_drag_points.clear(); @@ -1239,6 +1241,146 @@ void editor_mode::push_track_snapshot(std::vectorm_paths.front()}; + watch.before.emplace_back(path.points[segment_data::point::start], path.points[segment_data::point::end]); + if (track->eType != tt_Normal) + continue; + if (auto const found{alignment_store::on_track(*track)}) + watch.on[found->record].push_back(track); + } + for (auto const &entry : watch.on) + { + bool whole{true}; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Normal && false == track->m_paths.empty() && + std::find(watch.moving.begin(), watch.moving.end(), track) == watch.moving.end() && lies_on(entry.first, *track)) + { + whole = false; + break; + } + if (whole) + watch.whole.insert(entry.first); + } +} + +// what the drag of the gizmo did to the kept alignments and switches: an alignment all of whose paths were moved or turned as a +// whole goes along with them, paths pulled out of an alignment or given another cant are told about, as are switches bent out +// of the shape of their type; switches moved as a whole take their records along +void editor_mode::records_watch_end() +{ + auto &watch{m_record_watch}; + if (false == watch.active) + return; + watch.active = false; + auto const index = [&](TTrack const *Track) { return static_cast(std::find(watch.moving.begin(), watch.moving.end(), Track) - watch.moving.begin()); }; + auto const chord = [](glm::dvec3 const &Start, glm::dvec3 const &End) { + auto const direction{plan_of(End - Start)}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; + }; + std::vector notes; + int followed{0}; + double departure{0.0}, cant{0.0}; + int departed{0}; + std::string layer; + for (auto const &[record, tracks] : watch.on) + { + if (watch.whole.count(record) > 0) + { + auto const &first{watch.before[index(tracks.front())]}; + auto const &path{tracks.front()->m_paths.front()}; + auto const from{chord(first.first, first.second)}; + auto const to{chord(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + auto const angle{signed_angle(from, to)}; + bool rigid{true}; + for (auto *track : tracks) + { + auto const &was{watch.before[index(track)]}; + auto const &now{track->m_paths.front()}; + for (auto const &[old, current] : {std::pair{was.first, now.points[segment_data::point::start]}, std::pair{was.second, now.points[segment_data::point::end]}}) + rigid = rigid && glm::distance(plan_of(path.points[segment_data::point::start]) + turned(plan_of(old) - plan_of(first.first), angle), plan_of(current)) <= 0.01; + } + if (rigid) + { + bool const moved{glm::distance(first.first, path.points[segment_data::point::start]) > 1e-4 || std::abs(angle) > 1e-7}; + if (moved) + { + alignment_store::transform(record, first.first, from, path.points[segment_data::point::start], to); + ++followed; + } + continue; + } + } + auto const &records{alignment_store::records()}; + auto const gauge{records[record].design.norms.gauge}; + for (auto *track : tracks) + { + if (track->m_editorremoved) + continue; + auto const &path{track->m_paths.front()}; + bezier const curve{path}; + double largest{0.0}; + for (auto const t : {0.0, 0.5, 1.0}) + { + auto const found{alignment_store::on_record(record, curve.point(t), 1000.0)}; + largest = std::max(largest, found.has_value() ? found->offset : 1000.0); + } + if (largest > alignment_store::tolerance) + { + ++departed; + departure = std::max(departure, largest); + layer = scene::Layers.layer(records[record].layer).name; + continue; + } + for (auto const end : {0, 1}) + if (auto const found{alignment_store::on_record(record, curve.point(end), alignment_store::tolerance)}) + { + auto const actual{gauge * std::sin(std::abs(glm::radians(static_cast(path.rolls[end]))))}; + cant = std::max(cant, std::abs(actual - alignment_store::cant_at(record, found->chainage))); + } + } + } + if (followed > 0) + notes.push_back(STR("The kept design of the line went along with its paths")); + if (departed > 0) + notes.push_back(format(STR_C("%d path(s) left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the route design lays them by the design again or forgets it"), departed, layer.c_str(), departure)); + if (cant > 5.0) + notes.push_back(format(STR_C("The cant differs from the kept design by up to %.0f mm"), cant)); + switch_store::follow(); + for (auto *track : watch.moving) + if (track->eType == tt_Switch && false == track->m_editorremoved) + if (auto const known{switch_store::named(*track)}; known.has_value() && false == switch_store::of(*track).has_value()) + notes.push_back(format(STR_C("Switch %s lost the shape of its type %s: Ctrl+Z takes it back"), track->name().c_str(), switch_store::label(*known).c_str())); + records_to_history(); + if (false == notes.empty()) + { + std::string text; + for (auto const ¬e : notes) + text += (text.empty() ? "" : "\n") + note; + ui()->set_status(text, departed > 0 || cant > 5.0 || notes.size() > static_cast(followed > 0 ? 1 : 0)); + WriteLog("Editor: " + text, logtype::generic); + } + watch = {}; +} + void editor_mode::records_to_history() { auto alignments{alignment_store::take_changes()}; @@ -5291,6 +5433,7 @@ void editor_mode::point_drag_update() for (auto *dragged : m_track_drag) states.emplace_back(dragged, editor_track::capture(*dragged)); push_track_snapshot(std::move(states)); + records_watch_begin(); } screen_projection const projection; glm::dvec3 position; @@ -5324,6 +5467,7 @@ void editor_mode::point_drag_finish() } m_track_snap = {}; commit_track_drag(true); + records_watch_end(); m_track_drag.clear(); m_track_drag_points.clear(); m_track_joints.clear(); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index f478186b..c43ad29d 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -1307,6 +1307,7 @@ bool editor_mode::track_set_begin(bool const Rotate) for (auto *track : m_track_drag) states.emplace_back(track, editor_track::capture(*track)); push_track_snapshot(std::move(states)); + records_watch_begin(); track_gizmo_hold(); return true; } @@ -1340,6 +1341,7 @@ void editor_mode::track_set_end() track_gizmo_settle(); m_track_dirty = true; commit_track_drag(true); + records_watch_end(); m_track_set_using = false; m_track_drag.clear(); m_track_set_joints.clear(); diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index c45d21f2..18506313 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -607,6 +607,49 @@ void forget(int const Index) mark(layer); } +std::optional on_record(int const Index, glm::dvec3 const &Point, double const Reach) +{ + load(); + if (Index < 0 || Index >= static_cast(g_records.size())) + return std::nullopt; + return project(static_cast(Index), plan_of(Point), Reach); +} + +double cant_at(int const Index, double const Chainage) +{ + load(); + if (Index < 0 || Index >= static_cast(g_records.size())) + return 0.0; + return alignment::evaluate(g_records[Index].result, Chainage).cant; +} + +void transform(int const Index, glm::dvec3 const &From, glm::dvec2 const &Fromdirection, glm::dvec3 const &To, glm::dvec2 const &Todirection) +{ + load(); + if (Index < 0 || Index >= static_cast(g_records.size())) + return; + auto &entry{g_records[Index]}; + auto const angle{geometry::signed_angle(glm::normalize(Fromdirection), glm::normalize(Todirection))}; + auto const lift{To.y - From.y}; + auto const place = [&](glm::dvec3 const &Point) { + auto const planar{plan_of(To) + geometry::turned(plan_of(Point) - plan_of(From), angle)}; + return glm::dvec3{planar.x, Point.y + lift, planar.y}; + }; + auto &design{entry.design}; + design.start = place(design.start); + design.end = place(design.end); + design.start_direction = geometry::turned(design.start_direction, angle); + design.end_direction = geometry::turned(design.end_direction, angle); + for (auto &vertex : design.vertices) + vertex.position = plan_of(place({vertex.position.x, 0.0, vertex.position.y})); + finish(entry); + entry.elements.clear(); + for (auto const &e : entry.result.elements) + entry.elements.push_back(describe(e)); + g_changed = true; + mark(entry.layer); +} + std::optional chainage_at(glm::dvec3 const &Point) { auto const found{at(Point)}; diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp index 55fb189e..7d275bcc 100644 --- a/editor/editorAlignmentStore.hpp +++ b/editor/editorAlignmentStore.hpp @@ -85,6 +85,12 @@ 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); +// the point on the line of the stored alignment, if it lies by it within the reach +std::optional on_record(int const Index, glm::dvec3 const &Point, double const Reach); +// cant of the design at the chainage, mm +double cant_at(int const Index, double const Chainage); +// the stored alignment moved and turned in the plan the way the point and the direction were +void transform(int const Index, glm::dvec3 const &From, glm::dvec2 const &Fromdirection, glm::dvec3 const &To, glm::dvec2 const &Todirection); // km of the point on the stored alignments, if it lies on one std::optional chainage_at(glm::dvec3 const &Point); // the records as they were before the changes made since the last call, if there were any; the current ones become the base diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index 2db7ec62..b780e785 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorSwitchStore.hpp" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" +#include "editor/editorAlignmentStore.hpp" #include "world/Track.h" #include "simulation/simulation.h" @@ -39,7 +40,8 @@ bool g_loaded{false}; // format of the lines, coordinates in the space of the file: // -// parts ... start dir km number source +// parts ... shape
start dir +// km number source std::string word(std::string const &Text) { if (Text.empty()) @@ -69,6 +71,32 @@ bool in_place(record const &Record, TTrack const &Track) return geometry::plan_distance(Track.m_paths.front().points[segment_data::point::start], Record.start) <= kTolerance && glm::dot(direction_of(Track), Record.direction) > 0.9999; } +glm::dvec4 measure(TTrack const &Track) +{ + auto const &main{Track.m_paths.front()}; + auto const start{plan_of(main.points[segment_data::point::start])}; + auto const along{direction_of(Track)}; + glm::dvec2 const across{-along.y, along.x}; + auto const local = [&](glm::dvec3 const &Point) { + auto const offset{plan_of(Point) - start}; + return glm::dvec2{glm::dot(offset, along), glm::dot(offset, across)}; + }; + auto const mainend{local(main.points[segment_data::point::end])}; + auto const branchend{Track.m_paths.size() > 1 ? local(Track.m_paths[1].points[segment_data::point::end]) : glm::dvec2{0.0}}; + return {mainend.x, mainend.y, branchend.x, branchend.y}; +} + +bool in_shape(record const &Record, TTrack const &Track) +{ + if (false == Record.shaped) + return in_place(Record, Track); + auto const now{measure(Track)}; + for (int i = 0; i < 4; ++i) + if (std::abs(now[i] - Record.shape[i]) > kTolerance) + return false; + return true; +} + void mark(scene::layer_handle const Layer) { if (false == scene::Layers.valid(Layer)) @@ -89,6 +117,8 @@ void mark(scene::layer_handle const Layer) entry.parts.size())}; for (auto const &part : entry.parts) text += ' ' + word(part); + if (entry.shaped) + text += format(" shape %.4f %.4f %.4f %.4f", entry.shape.x, entry.shape.y, entry.shape.z, entry.shape.w); text += format(" start %.4f %.4f %.4f dir %.9f %.9f km ", start.x, start.y, start.z, direction.x, direction.y); text += entry.km.has_value() ? format("%.3f", *entry.km / 1000.0) : std::string{"-"}; text += " number " + word(entry.number) + " source " + word(entry.source); @@ -135,7 +165,13 @@ void load() line >> part; glm::dvec3 start{0.0}, ahead{0.0}; glm::dvec2 direction{0.0, 1.0}; - line >> keyword >> start.x >> start.y >> start.z >> keyword >> direction.x >> direction.y >> keyword >> km >> keyword >> entry.number >> keyword >> entry.source; + line >> keyword; + if (keyword == "shape") + { + line >> entry.shape.x >> entry.shape.y >> entry.shape.z >> entry.shape.w >> keyword; + entry.shaped = true; + } + line >> start.x >> start.y >> start.z >> keyword >> direction.x >> direction.y >> keyword >> km >> keyword >> entry.number >> keyword >> entry.source; if (line.fail()) continue; entry.start = context.to_world(start); @@ -159,23 +195,51 @@ std::vector const &records() return g_records; } -std::optional of(TTrack const &Track) +std::optional named(TTrack const &Track) { load(); auto const &name{Track.name()}; if (name.empty()) return std::nullopt; for (auto const &entry : g_records) - { - if (std::find(entry.parts.begin(), entry.parts.end(), name) == entry.parts.end()) - continue; - auto const *first{usable(entry.parts.front())}; - if (first != nullptr && in_place(entry, *first)) + if (std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end()) return entry; - } return std::nullopt; } +std::optional of(TTrack const &Track) +{ + auto const found{named(Track)}; + if (false == found.has_value()) + return std::nullopt; + auto const *first{usable(found->parts.front())}; + if (first == nullptr || false == in_shape(*found, *first)) + return std::nullopt; + return found; +} + +std::vector follow() +{ + load(); + std::vector moved; + std::set layers; + for (auto &entry : g_records) + { + auto const *first{usable(entry.parts.front())}; + if (first == nullptr || false == entry.shaped || false == in_shape(entry, *first) || in_place(entry, *first)) + continue; + entry.start = first->m_paths.front().points[segment_data::point::start]; + entry.direction = direction_of(*first); + entry.km = alignment_store::chainage_at(entry.start); + moved.push_back(entry.name); + layers.insert(entry.layer); + } + for (auto const layer : layers) + mark(layer); + g_changed = g_changed || false == moved.empty(); + return moved; +} + double branch_radius(record const &Record) { if (Record.radius <= 0.0) @@ -188,6 +252,11 @@ double branch_radius(record const &Record) void put(record Entry, scene::layer_handle const Layer) { load(); + if (auto const *first{usable(Entry.parts.empty() ? std::string{} : Entry.parts.front())}) + { + Entry.shape = measure(*first); + Entry.shaped = true; + } auto const existing{std::find_if(g_records.begin(), g_records.end(), [&](record const &Other) { return Other.name == Entry.name && Other.kind == Entry.kind; })}; if (existing != g_records.end()) { @@ -222,6 +291,7 @@ void forget(std::string const &Name) void tidy() { load(); + follow(); std::set layers; for (auto &entry : g_records) { diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 83d8c098..3cca9f3a 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -38,6 +38,10 @@ struct record std::vector parts; // the switch, the switches of a slip, the paths of a crossing glm::dvec3 start{0.0}; // of the first part glm::dvec2 direction{0.0, 1.0}; + // ends of the main and the diverging track of the first part seen from its start: along, across, along, across. it stays the + // same as long as the switch is moved or turned as a whole, it changes when the switch gets bent out of its shape + glm::dvec4 shape{0.0}; + bool shaped{false}; std::optional km; std::string number{"-"}; std::string source{"designed"}; // designed: from the catalogue, measured: copied from a switch of the scenery @@ -46,8 +50,12 @@ struct record void reset(); void load(); std::vector const &records(); -// the record of the switch, slip or crossing the track is a part of, while its first part starts where the record says +// the record of the switch, slip or crossing the track is a part of, while its first part keeps the shape the record says std::optional of(TTrack const &Track); +// the record of the track by its name alone, also when the switch lost its shape +std::optional named(TTrack const &Track); +// records whose switches were moved or turned as a whole take their new place and km; returns their names +std::vector follow(); // radius of the diverging track: of the type, bent with the main track for an arc switch double branch_radius(record const &Record); // keeps the record in the file of the layer, in place of the one with the same name diff --git a/lang/editor_pl.po b/lang/editor_pl.po index f754f947..dbed1df1 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5379,3 +5379,15 @@ msgstr "Kilometraż początku rozjazdu; brany z planu sytuacyjnego zapisanego w msgid "Slip switches and crossings" msgstr "Rozjazdy krzyżowe i skrzyżowania" + +msgid "The kept design of the line went along with its paths" +msgstr "Zapisany projekt linii przesunął się razem z jej odcinkami" + +msgid "%d path(s) left the design kept in %s, by up to %.2f m: Ctrl+Z takes it back, the route design lays them by the design again or forgets it" +msgstr "Odcinki (%d) zeszły z projektu zapisanego w %s, maks. o %.2f m: Ctrl+Z cofa, projekt trasy ułoży je ponownie według projektu albo go zapomni" + +msgid "The cant differs from the kept design by up to %.0f mm" +msgstr "Przechyłka różni się od zapisanego projektu maks. o %.0f mm" + +msgid "Switch %s lost the shape of its type %s: Ctrl+Z takes it back" +msgstr "Rozjazd %s stracił kształt swojego typu %s: Ctrl+Z cofa"