diff --git a/application/editormode.h b/application/editormode.h index 772d5c49..6aaf58a7 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1780,6 +1780,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri void render_straight_ui(); track_tab m_track_tab{track_tab::path}; void delete_selected_track(); + std::string designs_trimmed(std::vector const &Removed); void start_straight_gesture(int const Tool); void finish_straight_gesture(); void toggle_straight_set(TTrack &Track); diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index c2cd7e2b..bd3fd6a4 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -536,12 +536,12 @@ bool editor_mode::digitize_chain(editor_track::chain const &Chain, std::string * for (auto const index : covered) { auto const &old{records[index]}; - auto const start{alignment_store::chainage_on(result, old.design.start)}; - auto const end{alignment_store::chainage_on(result, old.design.end)}; + auto const start{alignment_store::chainage_on(result, alignment_store::kept_start(old))}; + auto const end{alignment_store::chainage_on(result, alignment_store::kept_end(old))}; if (false == start.has_value() || false == end.has_value()) return false; if (index == *covered.begin() && *end > *start) - origin = old.origin - *start; + origin = old.origin + old.from - *start; } auto const layer{covered.empty() ? alignment_store::layer_of(*Chain.tracks.front()) : records[*covered.begin()].layer}; for (auto index{covered.rbegin()}; index != covered.rend(); ++index) diff --git a/application/editormodeselftesttrack.cpp b/application/editormodeselftesttrack.cpp index 97f59268..05cb7a40 100644 --- a/application/editormodeselftesttrack.cpp +++ b/application/editormodeselftesttrack.cpp @@ -398,7 +398,8 @@ editor_mode::selftest_result editor_mode::selftest_track(std::string const &comm 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)); + WriteLog(format("SELFTEST alignment %zu in %s: km %.3f, %zu vertices, %zu elements, consistent %d, length %.3f, kept %.3f - %.3f, %zu clicks", 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, + entry.from, entry.kept_to(), entry.clicks.size())); 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)); } diff --git a/application/editormodeswitchslide.cpp b/application/editormodeswitchslide.cpp index d58b9b5b..756af589 100644 --- a/application/editormodeswitchslide.cpp +++ b/application/editormodeswitchslide.cpp @@ -436,6 +436,8 @@ void editor_mode::lay_switch_branch(switch_branch const &Branch, editor_track::e for (std::size_t i = 0; i < records.size(); ++i) { auto const &record{records[i]}; + if (false == record.whole()) + continue; auto const atstart{plan_distance(record.design.start, from) < geometry::same_point}; if (false == atstart && plan_distance(record.design.end, from) >= geometry::same_point) continue; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index a43ef289..c06860f9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1596,6 +1596,14 @@ void editor_mode::render_path_ui() +// the designs kept for the lines the tracks deleted lay along cut down to the tracks left, in the same step of the undo history: what +// the status tells of it +std::string editor_mode::designs_trimmed(std::vector const &Removed) +{ + auto const cut{alignment_store::trim(Removed)}; + return cut > 0 ? std::string{STR_P("The design kept for the line is cut down to the tracks left along it", "The designs kept for the lines are cut down to the tracks left along them", static_cast(cut))} : std::string{}; +} + void editor_mode::delete_selected_track() { auto *track{selected_track()}; @@ -1630,6 +1638,8 @@ void editor_mode::delete_selected_track() created.push_back(straight); ui()->set_status(STR_C("The double switch is replaced by its main track, Shift+Del removes all of it"), false); } + if (auto const note{designs_trimmed({set->first, set->link, set->second})}; false == note.empty()) + ui()->set_status(note, false); push_track_snapshot({}, created, {set->first, set->link, set->second}); if (std::any_of(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), [&](TTrack const *Each) { return Each == set->first || Each == set->link || Each == set->second; })) m_route = {}; @@ -1653,6 +1663,8 @@ void editor_mode::delete_selected_track() created.push_back(main); ui()->set_status(STR_C("The switch is replaced by its main track, Shift+Del removes all of it"), false); } + if (auto const note{designs_trimmed({track})}; false == note.empty()) + ui()->set_status(note, false); push_track_snapshot({}, created, {track}); // the model and the events of a turntable can't stay without its track if (track->eType == tt_Table) diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index d58bcea4..bcba0273 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -1829,6 +1829,7 @@ void editor_mode::track_set_delete() if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) m_route = {}; } + auto const designs{designs_trimmed(removed)}; push_track_snapshot({}, {}, removed); m_track_set.clear(); m_node = nullptr; @@ -1838,6 +1839,8 @@ void editor_mode::track_set_delete() auto status{format(STR_P("%zu track deleted, Ctrl+Z brings it back", "%zu tracks deleted, Ctrl+Z brings them back", static_cast(removed.size())), removed.size())}; if (kept > 0) status += format(STR_C(", %zu with vehicles on them or in the files which can't be changed are left"), kept); + if (false == designs.empty()) + status += "\n" + designs; ui()->set_status(status, false); } diff --git a/editor/editorAlignmentStore.cpp b/editor/editorAlignmentStore.cpp index b9b7f19d..41199ab0 100644 --- a/editor/editorAlignmentStore.cpp +++ b/editor/editorAlignmentStore.cpp @@ -51,6 +51,7 @@ bool g_loaded{false}; // [ramps ]: the cant ramps of the curve where they don't go along its transitions, // their outer ends from the start and from the end of the curve, into it above 0; a length below 0 keeps the ramp along the transition // click : a point clicked when the line was laid, its curves fitted to them +// kept : the stretch of the line the design is kept for, chainages from its start, where tracks along it were deleted // element // radii signed with the turn, 0 for no curvature // end @@ -75,13 +76,15 @@ glm::dvec2 world_direction(scene::layer_context const &Context, glm::dvec3 const return glm::length(direction) > 1e-12 ? glm::normalize(direction) : Direction; } +// the points of the line every kStep from the start of the stretch it's kept for void sample(record &Record) { Record.samples.clear(); - auto const length{Record.result.length}; - auto const count{static_cast(std::ceil(length / kStep))}; + auto const from{std::clamp(Record.from, 0.0, Record.result.length)}; + auto const to{std::clamp(Record.kept_to(), from, Record.result.length)}; + auto const count{static_cast(std::ceil((to - from) / 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.samples.push_back(plan_of(alignment::evaluate(Record.result, std::min(to, from + i * kStep)).position)); Record.high = Record.samples.empty() ? glm::dvec2{0.0} : Record.samples.front(); Record.low = Record.high; for (auto const &point : Record.samples) @@ -140,6 +143,8 @@ void mark(scene::layer_handle const Layer) 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)); + if (false == entry.whole()) + lines.push_back(format("kept %.4f %.4f", entry.from, entry.kept_to())); for (auto const &vertex : design.vertices) { auto const position{context.to_local({vertex.position.x, 0.0, vertex.position.y})}; @@ -225,7 +230,7 @@ int merge(record const &Entry) for (std::size_t r = 0; r < g_records.size(); ++r) { auto const &others{g_records[r].result.vertices}; - if (others.size() < vertices.size()) + if (others.size() < vertices.size() || false == g_records[r].whole()) continue; for (auto const backwards : {false, true}) for (std::size_t first = 0; first + vertices.size() <= others.size(); ++first) @@ -420,6 +425,16 @@ void read_part(record &Entry, std::string const &Keyword, std::istringstream &Li if (Line >> point.x >> point.y >> point.z) Entry.clicks.push_back(Context.to_world(point)); } + else if (Keyword == "kept") + { + double from; + double to; + if (Line >> from >> to && to > from) + { + Entry.from = std::max(0.0, from); + Entry.to = to; + } + } } void close_record(record &Entry, std::string const &Layer) @@ -519,8 +534,10 @@ std::optional project(record const &Entry, std::size_t const Index, glm::dv 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)}; + auto const from{entry.from}; + auto const to{entry.kept_to()}; + auto low{std::max(from, from + nearest * kStep - kStep)}; + auto high{std::min(to, from + nearest * kStep + kStep)}; for (int i = 0; i < 60; ++i) { auto const a{low + (high - low) / 3.0}; @@ -534,10 +551,10 @@ std::optional project(record const &Entry, std::size_t const Index, glm::dv auto const offset{away(chainage)}; if (offset > reach) return std::nullopt; - if (chainage < 1e-3 || chainage > entry.result.length - 1e-3) + if (chainage < from + 1e-3 || chainage > to - 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)}; + auto const outward{glm::dot(Point - plan_of(end.position), glm::normalize(plan_of(end.direction))) * (chainage < from + 1e-3 ? -1.0 : 1.0)}; if (outward > tolerance + entry.fit) return std::nullopt; } @@ -590,15 +607,15 @@ std::vector> bare(std::size_t const Index) } std::sort(covered.begin(), covered.end()); std::vector> gaps; - double reached{0.0}; + double reached{entry.from}; for (auto const &[from, to] : covered) { if (from - reached > 0.5) gaps.emplace_back(reached, from); reached = std::max(reached, to); } - if (entry.result.length - reached > 0.5) - gaps.emplace_back(reached, entry.result.length); + if (entry.kept_to() - reached > 0.5) + gaps.emplace_back(reached, entry.kept_to()); return gaps; } @@ -683,6 +700,11 @@ std::optional vertex_of(std::vector> } if (false == along || vertices.size() != 1 || *vertices.begin() >= static_cast(entry.design.vertices.size())) continue; + // a curve cut by the deletion of tracks along it is no designed one any more: the tracks left of it are recognized as they lie + if (std::any_of(entry.result.elements.begin(), entry.result.elements.end(), [&](alignment::element const &E) { + return E.vertex == *vertices.begin() && E.kind != alignment::element_kind::straight && (E.chainage < entry.from - 0.01 || E.chainage + E.length > entry.kept_to() + 0.01); + })) + 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()) @@ -706,6 +728,8 @@ int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed) for (std::size_t i = 0; i < g_records.size(); ++i) { auto const &design{g_records[i].design}; + if (false == g_records[i].whole()) + continue; 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) @@ -717,6 +741,73 @@ int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed) return -1; } +glm::dvec3 kept_start(record const &Record) +{ + return alignment::evaluate(Record.result, Record.from).position; +} + +glm::dvec3 kept_end(record const &Record) +{ + return alignment::evaluate(Record.result, Record.kept_to()).position; +} + +std::size_t trim(std::vector const &Removed) +{ + load(); + // the records the tracks deleted lay along, the ones they lay along before they were retired + std::set touched; + for (auto const *track : Removed) + if (track != nullptr) + for (std::size_t r = 0; r < g_records.size(); ++r) + if (std::any_of(track->m_paths.begin(), track->m_paths.end(), [&](segment_data const &Path) { return lies_along(g_records[r], Path); })) + touched.insert(r); + std::vector kept; + std::set layers; + std::size_t cut{0}; + for (auto r{touched.rbegin()}; r != touched.rend(); ++r) + { + auto const gaps{bare(*r)}; + if (gaps.empty()) + continue; + auto const entry{g_records[*r]}; + g_records.erase(g_records.begin() + static_cast(*r)); + layers.insert(entry.layer); + ++cut; + // the stretches between the gaps, with the points clicked along them; none too short for a track + std::vector plan; + for (auto const &click : entry.clicks) + plan.push_back(plan_of(click)); + auto const projections{alignment::project(entry.result, plan)}; + auto from{entry.from}; + auto const keep = [&](double const To) { + if (To - from < 1.0) + return; + auto piece{entry}; + piece.from = from; + piece.to = To; + piece.clicks.clear(); + for (std::size_t i = 0; i < entry.clicks.size() && i < projections.size(); ++i) + if (projections[i].chainage >= from - 1.0 && projections[i].chainage <= To + 1.0) + piece.clicks.push_back(entry.clicks[i]); + sample(piece); + kept.push_back(std::move(piece)); + }; + for (auto const &[start, end] : gaps) + { + keep(start); + from = end; + } + keep(entry.kept_to()); + } + if (cut == 0) + return 0; + g_records.insert(g_records.end(), kept.begin(), kept.end()); + g_changed = true; + for (auto const layer : layers) + mark(layer); + return cut; +} + int departed(std::vector const &Tracks, double &Departure) { load(); diff --git a/editor/editorAlignmentStore.hpp b/editor/editorAlignmentStore.hpp index 0d0dbdbb..08bdf0bd 100644 --- a/editor/editorAlignmentStore.hpp +++ b/editor/editorAlignmentStore.hpp @@ -53,6 +53,12 @@ struct record std::vector samples; // the points clicked when the line was laid, its curves fitted to them, so they can be fitted again std::vector clicks; + // the stretch of its line the design is kept for, chainages from its start: all of it, unless tracks along it were deleted and + // the design was cut down to the ones left + double from{0.0}; + double to{-1.0}; // below 0: to the end of the line + double kept_to() const { return to < 0.0 ? result.length : to; } + bool whole() const { return from <= 1e-6 && (to < 0.0 || to >= result.length - 1e-6); } }; struct hit @@ -82,8 +88,15 @@ std::optional at(glm::dvec3 const &Point, double const Reach = tolerance); 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 +// stored alignment with these ends, either way; none cut down to a part of its line int find(glm::dvec3 const &Start, glm::dvec3 const &End, bool &Reversed); +// the ends of the stretch of its line the record is kept for +glm::dvec3 kept_start(record const &Record); +glm::dvec3 kept_end(record const &Record); +// the stored alignments the tracks deleted lay along cut down to the stretches of their line other tracks still lie along, a design +// with none left forgotten: the part of a line with no track is no part of its design any more. Called before the deletion goes into +// the undo history, which takes the records along. The count of the designs cut down or forgotten +std::size_t trim(std::vector const &Removed); // 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; with no points clicked given it keeps the diff --git a/lang/editor_pl.po b/lang/editor_pl.po index d2cfa2dd..08a111d7 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -12297,3 +12297,9 @@ msgstr "Wyprowadza z niego tor: prostą, a w trybie łuku (Tab) łuk i prostą; msgid "Dragging over another free end: another way to join" msgstr "Przeciąganie nad innym wolnym końcem: inny sposób połączenia" + +msgid "The design kept for the line is cut down to the tracks left along it" +msgid_plural "The designs kept for the lines are cut down to the tracks left along them" +msgstr[0] "Projekt linii zapisany w scenerii przycięto do torów, które przy nim zostały" +msgstr[1] "Projekty linii zapisane w scenerii przycięto do torów, które przy nich zostały" +msgstr[2] "Projekty linii zapisane w scenerii przycięto do torów, które przy nich zostały" diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp index 3b5c5aba..7ffebbca 100644 --- a/scene/scenelayers.cpp +++ b/scene/scenelayers.cpp @@ -852,8 +852,8 @@ std::array const editormarks{"//$p", "//$h", "//$r", "//$b", "/ // of the trainsets after //$o. The editor's lines of them are told by their first word bool editor_line(char const *Mark, std::string const &Text) { - static std::array>, 5> const words{{ - {"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "vertex", "element", "end"}}, + static std::array>, 5> const words{{ + {"//$h", {"#", "alignment", "source", "ends", "norms", "pieces", "kept", "vertex", "click", "element", "end"}}, {"//$r", {"#", "switch", "slip", "crossing", "double", "connection"}}, {"//$o", {"on"}}, {"//$s", {"#", "station"}},