From c024d7c262563bfa5283f421aa664796b41ebc18 Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 21:32:34 +0200 Subject: [PATCH] feat(editor): the signals and the signs by a track go with it as the Plan lays it anew, square to the line, not along it A curve laid anew, its radius changed or made compound, or a straight moved: the paths of the chain are cut again where their joints stood, set square onto the new line, each keeping its stretch of the ground, so their events, their track circuits and the cuts of the block sections stay where they were instead of being handed on along the line; the parts between the switches alike, the old even sharing kept where the joints don't follow one another along the new line. The objects by a track taken for a change go with it by themselves: the signals and the warning discs it or its run reads, the SHP resonators by their events, and the controls of the block sections, the hectometre posts, the W17 markers and the other signs by the track nearest to them, within 12 m. They move square to the new axis keeping their offset and their turn, an object moved by hand meanwhile staying where it was put. Such a binding isn't kept in the scenery nor shown among the bound objects of the infra tool, which can still bind the object for good, and the undo and the redo take the objects back and forth with the tracks. The status of the Plan tells how many objects moved with the tracks. A path taking an object over the other way round keeps it on its side, turned as it was. --- application/editormode.h | 17 +++ application/editormodedigitize.cpp | 4 +- application/editormodeinfra.cpp | 208 ++++++++++++++++++++++++++-- application/editormoderoute.cpp | 65 +++++++-- application/editormodestraights.cpp | 8 +- editor/editorAlignment.cpp | 23 +++ editor/editorAlignment.hpp | 7 + editor/editorInfra.hpp | 3 + editor/editorTrack.cpp | 20 ++- editor/editorTrack.hpp | 5 +- lang/editor_pl.po | 6 + 11 files changed, 336 insertions(+), 30 deletions(-) diff --git a/application/editormode.h b/application/editormode.h index 161ee849..842cd07c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -2372,6 +2372,23 @@ class editor_mode : public application_mode, private editor_track::observer, pri std::vector m_infra_buffer; // states of the objects before the change in progress std::vector> m_drive_buffer; bool m_infra_suspended{false}; + // the point objects which go with a path: the signals and the warning discs it reads, the SHP resonators, and the controls of the + // block sections, the signs and the posts by the path nearest to them + struct point_object + { + scene::instance_handle include{0}; + glm::dvec3 location{0.0}; + TTrack *reader{nullptr}; + TTrack *nearest{nullptr}; // of one no path reads + infra::category group{infra::category::other}; + }; + std::vector m_point_objects; + std::size_t m_point_objects_revision{static_cast(-1)}; + std::size_t m_point_objects_count{0}; + void infra_adopt(TTrack &Track); + int infra_bound_count() const; // by the infra tool, not the ones a change of their track took along + std::set> m_infra_carried; // the objects their tracks moved since the tool cleared it + std::string infra_carried_note(); // a line of the status telling how many, empty for none; clears them void infra_load(); void infra_store(); void infra_forget(scene::basic_node const *Node); diff --git a/application/editormodedigitize.cpp b/application/editormodedigitize.cpp index 99ea6d26..75e76f81 100644 --- a/application/editormodedigitize.cpp +++ b/application/editormodedigitize.cpp @@ -389,9 +389,9 @@ std::string editor_mode::digitize_scenery(bool const Write) m_infra.scope = 3; infra_recognize(); report.found = static_cast(m_infra.candidates.size()); - auto const before{m_bindings.size()}; + auto const before{infra_bound_count()}; infra_bind_chosen(); - report.bound = static_cast(m_bindings.size() - before); + report.bound = infra_bound_count() - before; m_infra.scope = scope; } auto text{format(STR_C("Switches: %d, kept already %d, of the catalogue %d, measured %d, parts of slips %d, odd %d"), report.switches, report.known, report.designed, report.measured, report.slips, report.odd)}; diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp index 1fef9e8c..abc9f30a 100644 --- a/application/editormodeinfra.cpp +++ b/application/editormodeinfra.cpp @@ -49,6 +49,11 @@ using geometry::plan_distance; using geometry::plan_of; std::string const kBindingMark{"//$b"}; +// the templates of the signalling which go with a path without a signal template of their own: the SHP resonator, read by its +// event, and the control of a block section, by the path nearest to it +std::string const kResonatorFile{"rezonator.inc"}; +std::string const kControlFile{"sbl_odstep.inc"}; +double constexpr kAdoptReach{12.0}; // m, of a point object from the axis of its path template bool contains(std::vector const &Items, Type_ const &Item) @@ -803,7 +808,7 @@ void editor_mode::infra_store() { std::vector points; std::optional yaw; - if (binding.anchors.empty() || false == object_points(binding, points, yaw)) + if (binding.adopted || binding.anchors.empty() || false == object_points(binding, points, yaw)) continue; auto layer{scene::Layers.resolve(binding.anchors.front().at.track->layer())}; if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) @@ -870,6 +875,18 @@ infra::binding *editor_mode::infra_find(infra::binding const &Binding) return nullptr; } +std::string editor_mode::infra_carried_note() +{ + auto const count{m_infra_carried.size()}; + m_infra_carried.clear(); + return count > 0 ? format(STR_P("\n%zu object by the tracks moved sideways with them", "\n%zu objects by the tracks moved sideways with them", count), count) : std::string{}; +} + +int editor_mode::infra_bound_count() const +{ + return static_cast(std::count_if(m_bindings.begin(), m_bindings.end(), [](infra::binding const &Binding) { return false == Binding.adopted; })); +} + void editor_mode::infra_recognize() { auto &state{m_infra}; @@ -977,7 +994,8 @@ void editor_mode::infra_recognize() Binding.label = object_label(Binding); candidate.offset = Binding.anchors.front().offset; candidate.reason = Reason; - candidate.bound = (infra_find(Binding) != nullptr); + auto const *existing{infra_find(Binding)}; + candidate.bound = (existing != nullptr && false == existing->adopted); candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other && Binding.group != infra::category::environment); candidate.binding = std::move(Binding); state.read += candidate.reader != nullptr ? 1 : 0; @@ -1153,6 +1171,133 @@ void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh m_infra_buffer.push_back(infra_state_of(Binding)); } +void editor_mode::infra_adopt(TTrack &Track) +{ + auto const revision{history_revision()}; + if (revision != m_point_objects_revision || scene::Layers.instance_count() != m_point_objects_count) + { + m_point_objects_revision = revision; + m_point_objects_count = scene::Layers.instance_count(); + m_point_objects.clear(); + std::unordered_map readers; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + if (false == event.first.empty()) + readers.emplace(ToLower(event.first), track); + static auto const rules{infra::default_rules()}; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (false == include_place_of(handle, place) || place.values.empty() || false == scene::Layers.removable(handle)) + continue; + point_object object; + object.include = handle; + object.location = include_location(place); + auto const file{ToLower(place.file)}; + auto const *signal{signal_template_of(place.file)}; + std::string read; + if (signal != nullptr) + { + object.group = infra::category::signal; + read = signal->read.empty() ? std::string{} : editor_includes::substitute(signal->read, place.values); + } + else if (file == kResonatorFile) + { + object.group = infra::category::equipment; + read = place.values.front() + "_shp"; + } + else if (file == kControlFile) + object.group = infra::category::logic; + else + { + auto const named{infra::category_named(place.category)}; + object.group = named.has_value() ? *named : infra::classify(place.file + ' ' + place.description, rules); + if (object.group != infra::category::signal && object.group != infra::category::sign) + continue; + } + if (false == read.empty()) + if (auto const reader{readers.find(ToLower(read))}; reader != readers.end()) + object.reader = reader->second; + m_point_objects.push_back(std::move(object)); + } + // the path nearest to each one no path reads, the objects by cells of the plan + auto constexpr cell{200.0}; + auto const key = [&](double const Value) { return static_cast(std::floor(Value / cell)); }; + std::map, std::vector> cells; + for (std::size_t i = 0; i < m_point_objects.size(); ++i) + if (m_point_objects[i].reader == nullptr) + cells[{key(m_point_objects[i].location.x), key(m_point_objects[i].location.z)}].push_back(i); + std::vector nearest(m_point_objects.size(), kAdoptReach); + for (auto *track : simulation::Paths.sequence()) + { + if (cells.empty() || track == nullptr || track->m_editorremoved || false == editor_track::is_supported(*track)) + continue; + auto const area{area_of(*track)}; + auto const reach{area.radius + kAdoptReach}; + for (auto x = key(area.centre.x - reach); x <= key(area.centre.x + reach); ++x) + for (auto z = key(area.centre.z - reach); z <= key(area.centre.z + reach); ++z) + { + auto const found{cells.find({x, z})}; + if (found == cells.end()) + continue; + for (auto const i : found->second) + { + auto &object{m_point_objects[i]}; + if (plan_distance(object.location, area.centre) > area.radius + nearest[i]) + continue; + infra::station station; + bool interior; + auto const distance{infra::project(*track, object.location, station, interior)}; + if (distance < nearest[i] && beside(station, object.location, interior)) + { + nearest[i] = distance; + object.nearest = track; + } + } + } + } + } + auto const area{area_of(Track)}; + for (auto const &object : m_point_objects) + { + if (plan_distance(object.location, area.centre) > area.radius + kAdoptReach) + continue; + infra::binding binding; + binding.type = infra::kind::include; + binding.include = object.include; + if (infra_find(binding) != nullptr) + continue; + // of the line of the track: read by it or by a path of its run, else nearest to it; not one of the track alongside + if (object.reader != nullptr && object.reader != &Track) + { + auto const spans{editor_track::run_route(*object.reader, 250.0).spans}; + if (std::none_of(spans.begin(), spans.end(), [&](auto const &Span) { return Span.track == &Track; })) + continue; + } + if (object.reader == nullptr && object.nearest != &Track) + continue; + infra::station station; + bool interior; + if (infra::project(Track, object.location, station, interior) > kAdoptReach || false == beside(station, object.location, interior)) + continue; + std::vector points; + std::optional yaw; + if (false == object_points(binding, points, yaw) || points.empty()) + continue; + binding.group = object.group; + binding.anchors.push_back(infra::make_anchor(station, points.front())); + binding.turns = yaw.has_value() && object.group != infra::category::logic; + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + binding.adopted = true; + binding.measured = points.front(); + m_bindings.push_back(std::move(binding)); + } +} + void editor_mode::track_captured(TTrack const &Track) { sweeps_captured(Track); @@ -1160,6 +1305,8 @@ void editor_mode::track_captured(TTrack const &Track) if (m_infra_suspended || false == m_infra.follow) return; infra_load(); + // the signals and the like of the track follow it, staying where they stand along it + infra_adopt(const_cast(Track)); if (m_bindings.empty()) return; Live.refresh(); @@ -1188,6 +1335,8 @@ void editor_mode::track_captured(TTrack const &Track) if (binding.turns && yaw.has_value() && binding.anchors.front().at.track == &Track) binding.yaw = turn_of(binding, *yaw); } + if (binding.adopted && false == points.empty()) + binding.measured = points.front(); infra_buffer(binding, true); } } @@ -1223,6 +1372,7 @@ void editor_mode::tracks_committed(std::vector const &Tracks) // only a path running where the old one did, not the one alongside return nearest_beside(*everywhere, Foot, 1.0, Station); }; + std::vector dropped; for (auto &binding : m_bindings) { auto const affected{std::any_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { @@ -1230,6 +1380,14 @@ void editor_mode::tracks_committed(std::vector const &Tracks) })}; if (false == affected || false == alive(binding)) continue; + std::vector standing; + std::optional facing; + if (binding.adopted && (false == object_points(binding, standing, facing) || standing.empty() || glm::distance(standing.front(), binding.measured) > 0.01)) + { + // moved by another tool while its track was being changed: it stays where that put it, bound again with the next change + dropped.push_back(binding.include); + continue; + } auto moved{binding}; auto lost{false}; for (auto &anchor : moved.anchors) @@ -1256,23 +1414,53 @@ void editor_mode::tracks_committed(std::vector const &Tracks) } auto const offset{anchor.offset}; auto const height{anchor.height}; + // a path taking the object over may run the other way: the object stays on its side, turned as it was + auto const before{plan_of(anchor.end - anchor.start)}; anchor = infra::make_anchor(station, anchor.foot); - anchor.offset = offset; + auto const reversed{glm::length(before) > 1e-6 && glm::dot(before, infra::frame_at(anchor.at).forward) < 0.0}; + anchor.offset = reversed ? -offset : offset; anchor.height = height; + if (reversed && &anchor == &moved.anchors.front()) + moved.yaw = clamp_circular(moved.yaw + 180.0); } if (lost) { - binding.lost = true; + if (binding.adopted) + dropped.push_back(binding.include); + else + binding.lost = true; continue; } + if (binding.adopted) + { + // where nothing moved, the directive stays as it is written + auto const there{infra::place(moved.anchors.front())}; + auto const turned{facing.has_value() && moved.turns ? std::abs(clamp_circular(infra::heading(infra::frame_at(moved.anchors.front().at).forward) + moved.yaw - *facing + 180.0) - 180.0) : 0.0}; + if (glm::distance(there, standing.front()) < 0.005 && turned < 0.01) + { + moved.lost = false; + binding = std::move(moved); + continue; + } + } infra_buffer(binding, false); moved.lost = false; binding = std::move(moved); infra_move(binding); + m_infra_carried.emplace(binding.node, binding.include); + if (binding.adopted) + { + std::vector points; + std::optional yaw; + if (object_points(binding, points, yaw) && false == points.empty()) + binding.measured = points.front(); + } for (auto &entry : m_infra_buffer) if (entry.saved.same(binding)) entry.moved = true; } + if (false == dropped.empty()) + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), [&](infra::binding const &Binding) { return Binding.adopted && contains(dropped, Binding.include); }), m_bindings.end()); } void editor_mode::track_retired(TTrack &Track) @@ -1360,6 +1548,9 @@ void editor_mode::infra_attach(EditorSnapshot &Snapshot) auto const involved = [&](infra::object_state const &State) { if (State.moved) return true; + // one the editor bound for the change which didn't move: its own changes, if any, are taken back by the tool which made them + if (State.saved.adopted) + return false; for (auto const &anchor : State.saved.anchors) { if (contains(Snapshot.created, anchor.at.track) || contains(Snapshot.removed, anchor.at.track)) @@ -1394,7 +1585,7 @@ void editor_mode::render_infra_body() "models and includes turn with the path and stay upright. The move is a part of the undo step of the change.")); ImGui::SameLine(); ImGui::Checkbox(STR_C("Show in 3D"), &state.show); - ImGui::Text(STR_C("Objects bound in the scenery: %d"), static_cast(m_bindings.size())); + ImGui::Text(STR_C("Objects bound in the scenery: %d"), infra_bound_count()); if (false == m_bindings_unresolved.empty()) { ImGui::SameLine(); @@ -1612,9 +1803,9 @@ void editor_mode::render_infra_bound() std::unordered_set const tracks(scope.begin(), scope.end()); std::vector along; for (std::size_t i = 0; i < m_bindings.size(); ++i) - if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) + if (false == m_bindings[i].adopted && std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) along.push_back(i); - ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), static_cast(m_bindings.size())); + ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), infra_bound_count()); if (false == along.empty()) { ImGui::SameLine(); @@ -1678,7 +1869,8 @@ void editor_mode::draw_infra_overlay() const drawlist->AddCircleFilled(screen, Size, Colour); }; for (auto const &binding : m_bindings) - draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); + if (false == binding.adopted) + draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); auto const object = [&](infra_candidate const &Candidate) { std::vector points; std::optional yaw; diff --git a/application/editormoderoute.cpp b/application/editormoderoute.cpp index 187b55b5..8508b939 100644 --- a/application/editormoderoute.cpp +++ b/application/editormoderoute.cpp @@ -590,6 +590,7 @@ void editor_mode::route_lay() { auto &route{m_route}; route.status.clear(); + m_infra_carried.clear(); if (false == route.result.valid || route.chain.tracks.empty()) return; editor_track::chain chain; @@ -760,9 +761,10 @@ void editor_mode::route_lay() std::vector relaid; if (chain.switches.empty()) { - auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + std::vector intervals; + auto pieces{alignment::pieces_at(route.result, route.design, chain.tracks.size(), editor_track::chain_joints(chain), intervals)}; editor_track::keep_heights(chain, pieces); - relaid = editor_track::relay(chain, pieces, &to); + relaid = editor_track::relay(chain, pieces, &to, &intervals); } else if (false == route_relay_through_switches(chain, states, relaid, to)) { @@ -793,6 +795,7 @@ void editor_mode::route_lay() route.status = added > 0 ? format(STR_P("Re-laid %zu path, added %zu", "Re-laid %zu paths, added %zu", chain.tracks.size()), chain.tracks.size(), added) : format(STR_P("Re-laid %zu path", "Re-laid %zu paths", chain.tracks.size()), chain.tracks.size()); if (startextension > 0.0 || endextension > 0.0) route.status += format(STR_C(", the curve took over %.2f / %.2f m of the adjoining straights"), startextension, endextension); + route.status += infra_carried_note(); // the crossovers taken out before which fit the lines now if (false == route.relaying && false == route.crossovers_out.empty()) { @@ -844,14 +847,14 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, } auto const oldlength{before}; std::vector placements; - std::vector breaks; + std::vector ends; // of the switches along the new line for (std::size_t k = 0; k < Chain.switches.size(); ++k) { auto const &passage{Chain.switches[k]}; auto const &main{passage.track->m_paths.front()}; auto const length{bezier{main}.plan_length()}; auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; - if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) + if (from < 0.0 || from + length > result.length || (false == ends.empty() && from < ends.back() - 0.01)) { route.error = format(STR_C("The switch %s doesn't fit on the designed line"), name(passage.track).c_str()); return false; @@ -874,22 +877,50 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, return false; } placements.push_back({passage.track, from, length, entry, angle}); - breaks.push_back(from); - breaks.push_back(from + length); + ends.push_back(from); + ends.push_back(from + length); } - // the regular parts between the switches, one interval each, the switches taking the ones between + // the regular parts between the switches, cut at the joints of their paths set square onto the new line where they follow one + // another along it, else one interval each; the switches taking the ones between auto const parts{editor_track::chain_parts(Chain)}; + std::vector breaks; std::vector counts; - std::vector partof; + std::vector> owners; // of each interval: the part, -1 for none, and its path, -1 for any std::size_t part{0}; for (std::size_t k = 0; k <= Chain.switches.size(); ++k) { auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; - counts.push_back(last - first); - partof.push_back(last > first ? static_cast(part++) : -1); + if (last > first && part < parts.size()) + { + auto const inner{alignment::breaks_at(result, editor_track::chain_joints(parts[part]), k == 0 ? 0.0 : ends[2 * k - 1], k == Chain.switches.size() ? result.length : ends[2 * k])}; + if (last - first > 1 && inner.size() + 1 == last - first) + for (std::size_t j = 0; j < last - first; ++j) + { + if (j > 0) + breaks.push_back(inner[j - 1]); + counts.push_back(1); + owners.emplace_back(static_cast(part), static_cast(j)); + } + else + { + counts.push_back(last - first); + owners.emplace_back(static_cast(part), -1); + } + ++part; + } + else + { + counts.push_back(last - first); + owners.emplace_back(-1, -1); + } if (k < Chain.switches.size()) + { + breaks.push_back(ends[2 * k]); + breaks.push_back(ends[2 * k + 1]); counts.push_back(1); + owners.emplace_back(-1, -1); + } } if (part != parts.size()) { @@ -899,9 +930,15 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector intervals; auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; std::vector> groups(parts.size()); - for (std::size_t i = 0; i < pieces.size(); ++i) - if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) - groups[partof[intervals[i] / 2]].push_back(pieces[i]); + std::vector> paths(parts.size()); // of each piece of a part, its path, where its joints cut the line + for (std::size_t i = 0; i < pieces.size() && i < intervals.size(); ++i) + if (intervals[i] < owners.size() && owners[intervals[i]].first >= 0) + { + auto const &[owner, path]{owners[intervals[i]]}; + groups[owner].push_back(pieces[i]); + if (path >= 0) + paths[owner].push_back(static_cast(path)); + } for (std::size_t i = 0; i < parts.size(); ++i) if (groups[i].size() < parts[i].tracks.size()) { @@ -938,7 +975,7 @@ bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, auto group{groups[i]}; editor_track::keep_heights(parts[i], group); TTrack *last{nullptr}; - auto const added{editor_track::relay(parts[i], group, &last)}; + auto const added{editor_track::relay(parts[i], group, &last, paths[i].size() == group.size() ? &paths[i] : nullptr)}; if (last != nullptr && parts[i].tracks.back() == Chain.tracks.back()) Last = last; Created.insert(Created.end(), added.begin(), added.end()); diff --git a/application/editormodestraights.cpp b/application/editormodestraights.cpp index 239ced1e..d65d302c 100644 --- a/application/editormodestraights.cpp +++ b/application/editormodestraights.cpp @@ -786,6 +786,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co { auto &state{m_straights}; state.status.clear(); + m_infra_carried.clear(); struct span { TTrack *track; @@ -973,9 +974,10 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co state.status = refused(m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front()); return; } - auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + std::vector intervals; + auto pieces{alignment::pieces_at(m_route.result, m_route.design, m_route.chain.tracks.size(), editor_track::chain_joints(m_route.chain), intervals)}; editor_track::keep_heights(m_route.chain, pieces); - auto created{editor_track::relay(m_route.chain, pieces, &m_route.to)}; + auto created{editor_track::relay(m_route.chain, pieces, &m_route.to, &intervals)}; editor_track::commit(tracks); for (auto const &entry : splitstates) { @@ -996,7 +998,7 @@ void editor_mode::straight_reshape(editor_track::straight const &Line, double co std::string error; editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); straight_refresh(); - state.status = STR_C("Done: click a curve to adjust it; Ctrl+Z takes it back"); + state.status = STR_C("Done: click a curve to adjust it; Ctrl+Z takes it back") + infra_carried_note(); } double editor_mode::straight_tool_radius(editor_track::straight const &Line) const diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index c8a88a32..b0b501c1 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -2128,6 +2128,29 @@ trace_fit fit_trace(design const &Design, std::size_t const Vertex, std::vector< return best; } +std::vector breaks_at(result const &Result, std::vector const &Points, double const From, double const To) +{ + double constexpr apart{0.2}; + std::vector breaks; + auto previous{From}; + for (auto const &at : project(Result, Points)) + { + if (at.chainage < previous + apart || at.chainage > To - apart) + return {}; + breaks.push_back(previous = at.chainage); + } + return breaks; +} + +std::vector pieces_at(result const &Result, design const &Design, std::size_t const Count, std::vector const &Joints, std::vector &Intervals) +{ + Intervals.clear(); + auto const breaks{breaks_at(Result, Joints, 0.0, Result.length)}; + if (breaks.empty() || breaks.size() != Joints.size()) + return pieces(Result, Design, Count); + return pieces(Result, Design, Count, breaks, std::vector(breaks.size() + 1, 1), &Intervals); +} + std::size_t minimum_pieces(result const &Result, design const &Design) { std::size_t count{0}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index a96c00b8..c91801af 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -217,6 +217,13 @@ std::size_t minimum_pieces(result const &Result, design const &Design); // one more than the breaks; Intervals receives the number of the interval of each piece 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); +// the points of the plan set square onto the line, the chainages of them in their order between From and To; empty where they +// don't follow one another along it +std::vector breaks_at(result const &Result, std::vector const &Points, double const From, double const To); +// the pieces of the line ending at the joints of the old paths set square onto it, so that each old path keeps its stretch of the +// ground: Intervals receives the old path of each piece, or stays empty where the joints don't follow one another along the line +// and the pieces are cut as by pieces() alone +std::vector pieces_at(result const &Result, design const &Design, std::size_t const Count, std::vector const &Joints, std::vector &Intervals); struct join_end { diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp index a2f34c32..8bd7029d 100644 --- a/editor/editorInfra.hpp +++ b/editor/editorInfra.hpp @@ -117,6 +117,9 @@ struct binding bool turns{false}; // the object has a rotation which follows the path std::string label; bool lost{false}; // none of the paths around could take the object over + // bound by the editor itself when its track was taken for a change, as a signal its path reads: not kept in the scenery + bool adopted{false}; + glm::dvec3 measured{0.0}; // where an adopted object stood when its track was taken bool same(binding const &Other) const { diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 853ed3c7..edd60a4d 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -3346,18 +3346,22 @@ void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start, } } -std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces, TTrack **Last) +std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces, TTrack **Last, std::vector const *Intervals) { std::vector created; auto const count{Chain.tracks.size()}; if (count == 0 || Pieces.size() < count) return created; + // each path of the chain keeps the stretch of the ground it had, where the pieces were cut at its joints: its events and its + // track circuit stay where they were, not handed on along the line + auto const along{Intervals != nullptr && Intervals->size() == Pieces.size() && Intervals->front() == 0 && Intervals->back() + 1 == count && + std::adjacent_find(Intervals->begin(), Intervals->end(), [](std::size_t const A, std::size_t const B) { return B < A || B > A + 1; }) == Intervals->end()}; std::vector tracks; std::size_t previous{count}; for (std::size_t i = 0; i < Pieces.size(); ++i) { - auto const index{i * count / Pieces.size()}; + auto const index{along ? (*Intervals)[i] : i * count / Pieces.size()}; TTrack *track; bool forward; if (index != previous) @@ -3382,6 +3386,18 @@ std::vector editor_track::relay(chain const &Chain, std::vector editor_track::chain_joints(chain const &Chain) +{ + std::vector joints; + for (std::size_t i = 0; i + 1 < Chain.tracks.size() && i < Chain.forward.size(); ++i) + { + auto const &path{Chain.tracks[i]->m_paths.front()}; + auto const &end{path.points[Chain.forward[i] ? segment_data::point::end : segment_data::point::start]}; + joints.emplace_back(end.x, end.z); + } + return joints; +} + TTrack *editor_track::clone(TTrack const &Template, std::string const &Name) { std::ostringstream text; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index acaf502a..b42a1dd0 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -506,7 +506,10 @@ class editor_track static TTrack *create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name = {}); static TTrack *create_turntable(segment_data const &Path, TTrack const *Style, std::string const &Name); static void move_straights(std::vector const &Lines, std::vector> const &Ends); - static std::vector relay(chain const &Chain, std::vector const &Pieces, TTrack **Last = nullptr); + // Intervals: the path of the chain each piece takes the place of, else they're handed out in turn + static std::vector relay(chain const &Chain, std::vector const &Pieces, TTrack **Last = nullptr, std::vector const *Intervals = nullptr); + // the joints between the paths of the chain in its order, in the plan + static std::vector chain_joints(chain const &Chain); static void retire(TTrack &Track); // a retired path back in the scenery, found by its name again; it's committed by the caller static void revive(TTrack &Track); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 2019360c..9abad58e 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -9098,6 +9098,12 @@ msgstr[0] "Ułożono na nowo %zu tor" msgstr[1] "Ułożono na nowo %zu tory" msgstr[2] "Ułożono na nowo %zu torów" +msgid "\n%zu object by the tracks moved sideways with them" +msgid_plural "\n%zu objects by the tracks moved sideways with them" +msgstr[0] "\n%zu obiekt przy torach przesunął się z nimi w bok" +msgstr[1] "\n%zu obiekty przy torach przesunęły się z nimi w bok" +msgstr[2] "\n%zu obiektów przy torach przesunęło się z nimi w bok" + msgid ", the curve took over %.2f / %.2f m of the adjoining straights" msgstr ", łuk zajął %.2f / %.2f m przyległych prostych"