From ef592e64798c36a4f303759d8283bf1d75a403ce Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 26 Oct 2026 20:40:37 +0100 Subject: [PATCH] feat(editor): the drive of a switch stands by the tips of its blades, not at the joint before them, and goes with the switch deleted. A drive placed, changed or carried along with its switch is set where the blades begin, as they're shaped or where the diverging track is broken at the tip of a planed blade, along the main track there, its heading and its tilt taken there; the drives the earlier versions of the editor set at the joints are moved there once, as the scenery is opened and the templates are known, with no step of the undo history, to be saved with it. Deleting a switch, a double switch or a set of tracks takes the drives of the switches along in the same step, Ctrl+Z brings them back. The joint before the blades and the tips of the blades of the switch being placed or the one selected are marked across the track in the view, so the tips seen on the photo aren't taken for the start of the switch. Selftests bladetips, drivetips; on a copy of test42 the drive of z1 went from the joint to 1.248 m, deleted with z1, back with Ctrl+Z --- application/editormode.cpp | 16 ++++++++ application/editormode.h | 8 ++++ application/editormodeselftestswitch.cpp | 22 +++++++++++ application/editormodeswitchdrive.cpp | 35 +++++++++++++++++ application/editormodetrack.cpp | 49 ++++++++++++++++++++++++ application/editormodetrackux.cpp | 1 + editor/editorTrack.hpp | 5 +++ editor/editorTrackShape.cpp | 30 +++++++++++++-- lang/editor_pl.po | 12 ++++++ 9 files changed, 174 insertions(+), 4 deletions(-) diff --git a/application/editormode.cpp b/application/editormode.cpp index 30e1db72..e18a2520 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1346,6 +1346,21 @@ bool editor_mode::update() { Global.editor_tracks = (current_work_area() == work_area::tracks); selftest_step(); + // the drives of the switches go to the tips of their blades once, as the templates of the scenery are known + if (false == m_drives_checked) + { + EditorIncludes.start(); + if (EditorIncludes.scanned()) + { + m_drives_checked = true; + if (auto const moved{drives_to_blade_tips()}; moved > 0) + { + auto const text{format(STR_P("The drive of %zu switch is moved to the tips of its blades", "The drives of %zu switches are moved to the tips of their blades", static_cast(moved)), moved)}; + ui()->set_status(text, false); + WriteLog("Editor: " + text, logtype::generic); + } + } + } // after the tool has told its own status if (false == m_orphan_note.empty()) { @@ -1560,6 +1575,7 @@ bool editor_mode::update() draw_track_set(); draw_track_spread(); draw_build_overlay(); + draw_blade_tips(); draw_track_intent(); draw_tape(); m_speed.update(); diff --git a/application/editormode.h b/application/editormode.h index 6aaf58a7..b821f601 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1714,6 +1714,8 @@ class editor_mode : public application_mode, private editor_track::observer, pri bool extend_snap(editor_track::snap_target &Target) const; void update_build_tools(); void draw_build_overlay() const; + // the joint before the blades of the switch being placed or the one selected and the tips of its blades + void draw_blade_tips() const; // modes of the track editor, one at a time: path is the plain selection using track_tab = editor_track_tab; struct track_mode @@ -2089,6 +2091,12 @@ class editor_mode : public application_mode, private editor_track::observer, pri void apply_switch_drive_choice(TTrack &Switch); void switch_drive_captured(TTrack const &Track); void switch_drives_committed(std::vector const &Tracks); + // the drives of the switches deleted go with them, in the step of the undo history the deletion made last + void drives_deleted(std::vector const &Switches); + // the drives of all the switches of the scenery set at the tips of their blades, the ones the earlier versions of the editor set at the + // joint before them too: once, as the scenery is opened, with no step of the undo history; how many moved + std::size_t drives_to_blade_tips(); + bool m_drives_checked{false}; scene::layer_handle output_layer(std::string const &Part) override; // places them in the active layer or the given one, as one step of the undo. returns: how many were placed std::size_t place_templates(std::vector const &Items, std::string &Error, scene::layer_handle Layer = scene::layer_handle{}) override; diff --git a/application/editormodeselftestswitch.cpp b/application/editormodeselftestswitch.cpp index 25894069..0f2d5c99 100644 --- a/application/editormodeselftestswitch.cpp +++ b/application/editormodeselftestswitch.cpp @@ -50,6 +50,28 @@ http://mozilla.org/MPL/2.0/. editor_mode::selftest_result editor_mode::selftest_switch(std::string const &command, std::istringstream &words) { + // each switch: the tips of its blades and its drive from the joint before them; drivetips moves the drives there first + if (command == "bladetips" || command == "drivetips") + { + if (command == "drivetips") + WriteLog(format("SELFTEST drivetips: %zu moved", drives_to_blade_tips())); + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2) + continue; + auto const start{track->m_paths[0].points[segment_data::point::start]}; + std::string text{format("SELFTEST drive of %s: blades from %.3f m", track->name().c_str(), editor_track::blade_tip_of(*track))}; + for (int path = 0; path < 2; ++path) + if (auto const &shape{track->blade(path)}; shape.valid) + text += format(", blade %d %.3f - %.3f m", path, shape.tip, shape.heel); + if (auto const found{find_switch_drive(*track)}) + text += format(", drive %.3f m from the joint", geometry::plan_distance(found->standing.location, start)); + else + text += ", no drive"; + WriteLog(text); + } + return selftest_result::handled; + } if (command == "curvedswitch") { std::string name; diff --git a/application/editormodeswitchdrive.cpp b/application/editormodeswitchdrive.cpp index 9b19c419..37dd0d01 100644 --- a/application/editormodeswitchdrive.cpp +++ b/application/editormodeswitchdrive.cpp @@ -292,6 +292,41 @@ bool editor_mode::replace_switch_drive(TTrack &Switch, int const Family, bool co return true; } +void editor_mode::drives_deleted(std::vector const &Switches) +{ + if (m_history.empty()) + return; + for (auto const *track : Switches) + { + if (track == nullptr || track->eType != tt_Switch) + continue; + auto const found{find_switch_drive(*track)}; + if (false == found.has_value() || false == scene::Layers.removable(found->standing.instance) || scene::Layers.instance(found->standing.instance).removed) + continue; + scene::Layers.removed(found->standing.instance, true); + m_history.back().instances.push_back(found->standing.instance); + } +} + +std::size_t editor_mode::drives_to_blade_tips() +{ + // the drives the earlier versions of the editor set at the joint before the blades among them + std::vector switches; + std::vector> before; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch) + continue; + if (auto const found{find_switch_drive(*track)}; found.has_value() && scene::Layers.removable(found->standing.instance)) + { + switches.push_back(track); + before.emplace_back(found->standing.instance, scene::Layers.directive(found->standing.instance)); + } + } + switch_drives_committed(switches); + return static_cast(std::count_if(before.begin(), before.end(), [](auto const &Before) { return scene::Layers.directive(Before.first) != Before.second; })); +} + bool editor_mode::render_switch_drive_choice() { auto &tool{m_switch}; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index c06860f9..3f02fe31 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1641,6 +1641,7 @@ void editor_mode::delete_selected_track() 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}); + drives_deleted({set->first, 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 = {}; m_node = nullptr; @@ -1666,6 +1667,7 @@ void editor_mode::delete_selected_track() if (auto const note{designs_trimmed({track})}; false == note.empty()) ui()->set_status(note, false); push_track_snapshot({}, created, {track}); + drives_deleted({track}); // the model and the events of a turntable can't stay without its track if (track->eType == tt_Table) for (auto const instance : turntable_includes(*track)) @@ -2264,6 +2266,53 @@ void editor_mode::draw_track_hints() foreground->AddText(at, IM_COL32(255, 255, 255, 255), readout.c_str()); } +void editor_mode::draw_blade_tips() const +{ + segment_data main; + double tip{0.0}; + if (m_switch.placing && m_switch.connection < 0) + { + auto const preview{switch_preview()}; + if (preview.size() < 2) + return; + main = preview[0]; + tip = editor_track::blade_tip_of(preview[1]); + } + else if (auto const *selected{selected_track()}; selected != nullptr && selected->eType == tt_Switch && selected->m_paths.size() >= 2 && false == selected->m_editorremoved) + { + main = selected->m_paths[0]; + tip = editor_track::blade_tip_of(*selected); + } + else + return; + // on the photo the tips of the blades are seen where the switch seems to begin, the joint before them isn't + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + geometry::bezier const curve{main}; + auto const mark = [&](double const Distance, ImU32 const Colour, float const Width, char const *Label, int const Side) { + auto const t{curve.parameter(Distance)}; + auto const at{curve.point(t)}; + auto direction{plan_of(curve.first(t))}; + if (glm::length(direction) < 1e-9) + return; + direction = glm::normalize(direction); + glm::dvec3 const across{-direction.y * 1.6, 0.0, direction.x * 1.6}; + projection.line(drawlist, at - across, at + across, IM_COL32(0, 0, 0, 170), Width + 3.0f); + projection.line(drawlist, at - across, at + across, Colour, Width); + ImVec2 screen; + if (projection.project(at + across * static_cast(Side), screen)) + { + auto const size{ImGui::CalcTextSize(Label)}; + ImVec2 const corner{Side > 0 ? screen.x + 6.0f : screen.x - 6.0f - size.x, screen.y - 0.5f * size.y}; + drawlist->AddRectFilled({corner.x - 3.0f, corner.y - 1.0f}, {corner.x + size.x + 3.0f, corner.y + size.y + 1.0f}, IM_COL32(0, 0, 0, 160), 3.0f); + drawlist->AddText(corner, Colour, Label); + } + }; + mark(0.0, IM_COL32(220, 220, 220, 230), 2.0f, STR_C("joint before the blades"), -1); + if (tip > 0.05) + mark(tip, IM_COL32(255, 170, 40, 255), 3.0f, STR_C("tips of the blades"), 1); +} + void editor_mode::draw_build_overlay() const { screen_projection const projection; diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index bcba0273..737d86e3 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -1831,6 +1831,7 @@ void editor_mode::track_set_delete() } auto const designs{designs_trimmed(removed)}; push_track_snapshot({}, {}, removed); + drives_deleted(removed); m_track_set.clear(); m_node = nullptr; ui()->set_node(nullptr); diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 439a9cf0..c2e6e530 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -208,7 +208,12 @@ class editor_track glm::dvec2 tilt{0.0}; bool rightswitch{false}; }; + // the drive of the switch by the tips of its blades, where the rods throw them over static std::optional drive_spot_of(TTrack const &Switch); + // how far along the main track of the switch the tips of its blades lie, where the pre-blade stretch from the joint ends: as the + // blades are shaped, else where the diverging track is broken at the tip of a blade planed back from the start; 0 at the start + static double blade_tip_of(TTrack const &Switch); + static double blade_tip_of(segment_data const &Diverging); struct straight_axis { diff --git a/editor/editorTrackShape.cpp b/editor/editorTrackShape.cpp index dd626bdb..b56d27ae 100644 --- a/editor/editorTrackShape.cpp +++ b/editor/editorTrackShape.cpp @@ -299,6 +299,26 @@ glm::dvec3 editor_track::control_vector(TTrack const &Track, point_ref const &Co return point_position(Track, Control) - point_position(Track, end_of(Control)); } +double editor_track::blade_tip_of(segment_data const &Diverging) +{ + // the tip of a planed blade lies some metres from the start at most, a longer first piece is a part of the curve + if (Diverging.pieces.size() < 2) + return 0.0; + auto const length{bezier{Diverging.pieces.front()}.plan_length()}; + return length < 5.0 ? length : 0.0; +} + +double editor_track::blade_tip_of(TTrack const &Switch) +{ + if (Switch.eType != tt_Switch || Switch.m_paths.size() < 2) + return 0.0; + double tip{0.0}; + for (int path = 0; path < 2; ++path) + if (auto const &shape{Switch.blade(path)}; shape.valid && shape.tip > 0.0) + tip = tip > 0.0 ? std::min(tip, shape.tip) : shape.tip; + return tip > 0.0 ? tip : blade_tip_of(Switch.m_paths[1]); +} + std::optional editor_track::drive_spot_of(TTrack const &Switch) { if (Switch.eType != tt_Switch || Switch.m_paths.size() < 2) @@ -306,8 +326,10 @@ std::optional editor_track::drive_spot_of(TTrack const auto const &main{Switch.m_paths[0]}; auto const &diverging{Switch.m_paths[1]}; auto const start{main.points[segment_data::point::start]}; - auto const &control{main.points[segment_data::point::control1]}; - auto const along{control != glm::dvec3{} ? control : main.points[segment_data::point::end] - start}; + // by the tips of the blades, not at the joint before them + bezier const curve{main}; + auto const at{curve.parameter(blade_tip_of(Switch))}; + auto const along{curve.first(at)}; auto heading{plan_of(along)}; if (glm::length(heading) < 1e-9) return std::nullopt; @@ -315,11 +337,11 @@ std::optional editor_track::drive_spot_of(TTrack const glm::dvec2 const right{-heading.y, heading.x}; drive_spot spot; spot.rightswitch = glm::dot(plan_of(diverging.points[segment_data::point::end] - start), right) > 0.0; - spot.location = start; + spot.location = curve.point(at); auto const yaw{glm::degrees(std::atan2(heading.x, heading.y))}; spot.yaw = yaw < 0.0 ? yaw + 360.0 : yaw; auto const run{glm::length(plan_of(along))}; - auto const roll{static_cast(main.rolls[0])}; + auto const roll{glm::mix(static_cast(main.rolls[0]), static_cast(main.rolls[1]), at)}; spot.tilt = {-glm::degrees(std::atan(along.y / run)), roll}; if (Global.bRollFix) spot.location.y += 0.75 * std::abs(std::sin(glm::radians(roll))); diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 08a111d7..bab22d1b 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -12303,3 +12303,15 @@ msgid_plural "The designs kept for the lines are cut down to the tracks left alo 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" + +msgid "joint before the blades" +msgstr "styk przediglicowy" + +msgid "tips of the blades" +msgstr "ostrza iglic" + +msgid "The drive of %zu switch is moved to the tips of its blades" +msgid_plural "The drives of %zu switches are moved to the tips of their blades" +msgstr[0] "Napęd %zu rozjazdu przestawiony na ostrza iglic" +msgstr[1] "Napędy %zu rozjazdów przestawione na ostrza iglic" +msgstr[2] "Napędy %zu rozjazdów przestawione na ostrza iglic"