From a93ed67c58369a7d113ad6754b850294d83a36e9 Mon Sep 17 00:00:00 2001 From: maj00r Date: Tue, 13 Oct 2026 18:30:16 +0200 Subject: [PATCH] feat(editor): every switch laid in the editor gets the key frogs, a box in the Switch panel sets or clears it as one undo step; the editor rebuilds the tracks crossing a changed or removed one; self-test commands frog, crossings, frogkey and geomhash; Polish translation --- application/editormodeselftest.cpp | 152 +++++++++++++++++++++++++++++ application/editormodetrack.cpp | 10 ++ editor/editorTrack.cpp | 29 +++++- editor/editorTrack.hpp | 1 + lang/editor_pl.po | 6 ++ 5 files changed, 196 insertions(+), 2 deletions(-) diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 98d4a1e2..3996bee6 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -44,6 +44,10 @@ http://mozilla.org/MPL/2.0/. // wait // camera from the first point, looking at the second // select +// frog [name] logs where the inner rails of each switch cross: distance on both paths, 1:n, throat, nose, check rails +// crossings [name] logs where rails of tracks cross rails of other tracks, checks that each acute crossing has one partner and one nose +// frogkey name [on|off] sets or clears the frogs key of a switch (one undo step); logs the key, whether it is written, the frog and the crossings +// geomhash [name] logs the number of vertices and a hash of the drawn geometry of each rail track (rails, trackbed, cut faces), to compare builds // sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, // centre), top (origin, bottom, top, edge), mode (bend, repeat), step, // length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1) @@ -142,6 +146,154 @@ void editor_mode::selftest_step() ui()->set_node(m_node); WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track")); } + else if (command == "frog") + { + std::string name; + words >> name; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->SwitchExtension == nullptr) + continue; + if (false == name.empty() && track->name() != name) + continue; + auto const frog{track->frog()}; + if (false == frog.valid) + { + WriteLog(format("SELFTEST frog %s: no frog, paths %.3f and %.3f m, blades %.3f m", track->name().c_str(), track->SwitchExtension->Segments[0]->GetLength(), track->SwitchExtension->Segments[1]->GetLength(), frog.blades[0])); + continue; + } + auto const n{1.0 / std::tan(frog.angle)}; + glm::dvec3 point, across; + track->SwitchExtension->Segments[0]->plan_frame(frog.at[0], point, across); + auto const gauge{point + across * (frog.side[0] * 0.5 * std::abs(track->fTrackWidth))}; + auto const check{0.5 * std::clamp(0.5 * n, 3.6, 9.0)}; + WriteLog(format("SELFTEST frog %s: crossing at %.3f / %.3f m (%.3f %.3f %.3f), 1:%.2f, side %d, blades %.3f / %.3f m, throat %.3f, nose %.3f, wing end %.3f, check rails %.3f-%.3f / %.3f-%.3f m", track->name().c_str(), + frog.at[0], frog.at[1], gauge.x, gauge.y, gauge.z, n, frog.side[0], frog.blades[0], frog.blades[1], frog.at[0] - 0.045 * n, frog.at[0] + 0.012 * n, frog.at[0] + 0.15 * n, + frog.at[0] - 0.25 - check, frog.at[0] - 0.25 + check, frog.at[1] - 0.25 - check, frog.at[1] - 0.25 + check)); + } + } + else if (command == "geomhash") + { + std::string name; + words >> name; + auto const mix = [](std::uint64_t &Hash, float const Value) { + Hash = (Hash ^ static_cast(static_cast(std::llround(Value * 1e4)))) * 1099511628211ull; + }; + std::uint64_t total{1469598103934665603ull}; + std::size_t vertices{0}; + int tracks{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + if (false == name.empty() && track->name() != name) + continue; + std::uint64_t hash{1469598103934665603ull}; + std::size_t count{0}; + int chunks{0}; + for (auto const *sequence : {&track->Geometry1, &track->Geometry2, &track->GeometryCut1, &track->GeometryCut2}) + { + for (auto const &geometry : *sequence) + { + ++chunks; + for (auto const &vertex : GfxRenderer->Vertices(geometry)) + { + for (int i = 0; i < 3; ++i) + { + mix(hash, vertex.position[i]); + mix(hash, vertex.normal[i]); + } + mix(hash, vertex.texture.x); + mix(hash, vertex.texture.y); + ++count; + } + } + mix(hash, -1.f); + } + WriteLog(format("SELFTEST geom %s: chunks %d, vertices %zu, hash %016llx", track->name().c_str(), chunks, count, static_cast(hash))); + total = (total ^ hash) * 1099511628211ull; + vertices += count; + ++tracks; + } + WriteLog(format("SELFTEST geom total: tracks %d, vertices %zu, hash %016llx", tracks, vertices, static_cast(total))); + } + else if (command == "frogkey") + { + std::string name, value; + words >> name >> value; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->eType != tt_Switch || track->SwitchExtension == nullptr) + WriteLog(format("SELFTEST frogkey %s: no such switch", name.c_str())); + else + { + if (value == "on" || value == "off") + { + auto const before{editor_track::capture(*track)}; + track->SwitchExtension->m_frogs = (value == "on"); + push_track_snapshot({{track, before}}); + editor_track::commit_parameters(*track); + } + std::ostringstream text; + track->export_as_text(text); + WriteLog(format("SELFTEST frogkey %s: key %d, written %d, frog %d, crossings %zu, profile '%s'", name.c_str(), track->SwitchExtension->m_frogs ? 1 : 0, text.str().find(" frogs ") != std::string::npos ? 1 : 0, + track->frog().valid ? 1 : 0, track->rail_crossings().size(), track->m_profile1.first.c_str())); + } + } + else if (command == "crossings") + { + std::string name; + words >> name; + auto const started{std::chrono::steady_clock::now()}; + int tracks{0}; + int found{0}; + int acutes{0}; + int mismatched{0}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != 1) + continue; + if (false == name.empty() && track->name() != name) + continue; + ++tracks; + for (auto const &crossing : track->rail_crossings()) + { + ++found; + auto const kind{crossing.angle > 0.785 ? "wide" : crossing.facing < 0.0 ? "acute" : "obtuse"}; + WriteLog(format("SELFTEST crossing %s path %d side %d at %.3f m: %s with %s, 1:%.2f, into %.3f, facing %.3f, nose %d", track->name().c_str(), crossing.path, crossing.side, crossing.at, kind, + crossing.track->name().c_str(), 1.0 / std::tan(crossing.angle), crossing.into, crossing.facing, crossing.nose ? 1 : 0)); + } + auto const gauge = [](TTrack *Track, TTrack::rail_crossing const &Crossing) { + auto const &segment{Track->eType == tt_Switch ? *Track->SwitchExtension->Segments[Crossing.path] : *Track->Segment}; + glm::dvec3 point, across; + segment.plan_frame(Crossing.at, point, across); + return point + across * (Crossing.side * 0.5 * std::abs(Track->fTrackWidth)); + }; + for (auto const &crossing : track->rail_crossings(false)) + { + if (crossing.angle > 0.785 || crossing.facing >= 0.0) + continue; + ++acutes; + auto const here{gauge(track, crossing)}; + auto partners{0}; + auto noses{crossing.nose ? 1 : 0}; + for (auto const &other : crossing.track->rail_crossings(false)) + { + if (other.track != track || other.angle > 0.785 || other.facing >= 0.0) + continue; + auto const there{gauge(crossing.track, other)}; + if (glm::length(glm::dvec2{there.x - here.x, there.z - here.z}) > 0.05) + continue; + ++partners; + noses += other.nose ? 1 : 0; + } + if (partners != 1 || noses != 1) + ++mismatched; + WriteLog(format("SELFTEST acute %s %s path %d side %d at %.3f m with %s (%.2f %.2f %.2f): partners %d, noses %d", partners != 1 || noses != 1 ? "mismatch" : "ok", track->name().c_str(), crossing.path, crossing.side, crossing.at, crossing.track->name().c_str(), here.x, here.y, here.z, partners, noses)); + } + } + auto const elapsed{std::chrono::duration(std::chrono::steady_clock::now() - started).count()}; + WriteLog(format("SELFTEST crossings: %d rail crossings on %d tracks, %d acute, %d mismatched, %.1f ms", found, tracks, acutes, mismatched, elapsed)); + } else if (command == "sweep") { std::map values; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..7d2b4d21 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -1005,6 +1005,16 @@ void editor_mode::render_turnout_ui() if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch"))) place_switch_drive(*track); } + bool frogs{track->SwitchExtension->m_frogs}; + if (ImGui::Checkbox(STR_C("Frog and check rails made from the rails"), &frogs)) + { + auto const before{editor_track::capture(*track)}; + track->SwitchExtension->m_frogs = frogs; + push_track_snapshot({{track, before}}); + editor_track::commit_parameters(*track); + } + item_tooltip("Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\n" + "Off for a switch whose include has its own model of them"); if (false == m_switch.status.empty()) ImGui::TextWrapped("%s", m_switch.status.c_str()); if (angle > 1e-6) diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index 418d908f..bce8a02d 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -145,6 +145,7 @@ editor_track::state editor_track::capture(TTrack const &Track) result.material1 = Track.m_material1; result.material2 = Track.m_material2; result.material3 = Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle; + result.frogs = Track.SwitchExtension && Track.SwitchExtension->m_frogs; result.texlength = Track.fTexLength; result.texheight = texture_height(Track); result.texwidth = Track.fTexWidth; @@ -234,6 +235,7 @@ void editor_track::apply(TTrack &Track, state const &State) { Track.SwitchExtension->fVelocity = State.switchvelocity; Track.SwitchExtension->m_material3 = State.material3; + Track.SwitchExtension->m_frogs = State.frogs; } Track.fTrackWidth = State.width; Track.fTrackWidth2 = State.width; @@ -2404,6 +2406,7 @@ TTrack *editor_track::create_switch(switch_template const &Template, std::vector write_ending(text, Style); track = load_path(text.str(), Style, Name); } + track->SwitchExtension->m_frogs = true; track->m_paths.assign(Paths.begin(), Paths.begin() + 2); return track; } @@ -2820,12 +2823,34 @@ void editor_track::retire(TTrack &Track) update_transition(*neighbour); rebuild_geometry(*neighbour); } + auto crossings{Track.m_crossings}; + for (auto *crossing : Track.m_crossings) + for (auto *neighbour : neighbours(*crossing)) + if (std::find(crossings.begin(), crossings.end(), neighbour) == crossings.end()) + crossings.push_back(neighbour); + for (auto *crossing : crossings) + if (crossing != &Track && false == crossing->m_editorremoved) + rebuild_geometry(*crossing); if (s_observer != nullptr) s_observer->track_retired(Track); } void editor_track::rebuild_geometry(TTrack &Track) { - auto §ion{simulation::Region->section(Track.location())}; - Track.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{}); + auto const rebuild = [](TTrack &Path) { + auto §ion{simulation::Region->section(Path.location())}; + Path.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{}); + }; + auto crossings{Track.m_crossings}; + rebuild(Track); + for (auto *crossing : Track.m_crossings) + if (std::find(crossings.begin(), crossings.end(), crossing) == crossings.end()) + crossings.push_back(crossing); + for (std::size_t i = 0, count = crossings.size(); i < count; ++i) + for (auto *neighbour : neighbours(*crossings[i])) + if (std::find(crossings.begin(), crossings.end(), neighbour) == crossings.end()) + crossings.push_back(neighbour); + for (auto *crossing : crossings) + if (crossing != &Track && false == crossing->m_editorremoved) + rebuild(*crossing); } diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 43d42762..17603ce8 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -49,6 +49,7 @@ class editor_track material_handle material1{0}; material_handle material2{0}; material_handle material3{0}; + bool frogs{false}; float texlength{4.f}; float texheight{0.6f}; float texwidth{0.9f}; diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b30109..19a2b86a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5213,3 +5213,9 @@ msgstr "Punkt znikania" msgid "Pedestrian crossing" msgstr "Przejście dla pieszych" + +msgid "Frog and check rails made from the rails" +msgstr "Krzyżownica i kierownice z szyn" + +msgid "Cuts the rails of the switch at its frog and lays wing rails and check rails, for rail profiles S60, UIC60 and S39.\nOff for a switch whose include has its own model of them" +msgstr "Tnie szyny rozjazdu na krzyżownicy i układa szyny skrzydłowe i kierownice, dla profili szyn S60, UIC60 i S39.\nWyłącz dla rozjazdu, którego include ma ich własny model"