/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "application/editormode.h" #include "application/editormodeshared.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" #include "scene/scene.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" #include "utilities/Logs.h" #include "utilities/utilities.h" #include "world/Track.h" #include "imgui/imgui.h" #include "utilities/translation.h" #include #include #include namespace { using geometry::bezier; using geometry::plan_distance; using geometry::plan_of; double const kSampleStep{2.0}; // m between the samples of the line double const kLineCorridor{15.0}; // m from the axis, posts farther away belong to another line double const kOverlayRange{3000.0}; float const kFoulingPick{12.0f}; // px from a point of the fouling overlay a click takes it double const kFoulingTrace{250.0}; // m from the start of a switch the tracks are followed for double const kRailHeight{0.18}; // m from the points of a track to the heads of its rails int const kRailCategory{1}; char const *const kHektoStyles[] = {"nowy", "stary", "zgnity"}; char const *const kFoulingModels[] = {"ip/pkp/w17.inc", "ip/pkp/w17_new.inc", "ip/pkp/w17_old.inc", "ip/pkp/w17_dwuteo.inc"}; std::string normalized(std::string Text) { std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(Character == '\\' ? '/' : std::tolower(Character)); }); return Text; } bool numeric(std::string const &Text, double &Value) { char *end{nullptr}; Value = std::strtod(Text.c_str(), &end); return false == Text.empty() && *end == '\0'; } std::string number(double const Value, int const Decimals) { auto text{format("%.*f", Decimals, Value)}; if (text == "-0" || text.find_first_not_of("-0.") == std::string::npos) text.erase(0, text.front() == '-' ? 1 : 0); return text; } double heading_of(glm::dvec2 const &Direction) { auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))}; return degrees < 0.0 ? degrees + 360.0 : degrees; } // index of the sample at or before the chainage std::size_t sample_index(std::vector const &Samples, double const Chainage) { auto const found{std::upper_bound(Samples.begin(), Samples.end(), Chainage, [](double const Value, editor_track::route_sample const &Sample) { return Value < Sample.chainage; })}; return found == Samples.begin() ? 0 : std::min(std::distance(Samples.begin(), found) - 1, Samples.size() - 2); } glm::dvec2 sampled_direction(std::vector const &Samples, double const Chainage) { auto const index{sample_index(Samples, Chainage)}; auto const &a{Samples[index]}; auto const &b{Samples[index + 1]}; auto const span{b.chainage - a.chainage}; auto const f{span > 1e-9 ? std::clamp((Chainage - a.chainage) / span, 0.0, 1.0) : 0.0}; auto const direction{a.direction * (1.0 - f) + b.direction * f}; return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; } // chainage of the point of the line nearest to the point, and how far from the line it is double project(std::vector const &Samples, glm::dvec3 const &Point, double &Distance) { Distance = std::numeric_limits::max(); double chainage{0.0}; auto const point{plan_of(Point)}; for (std::size_t i = 1; i < Samples.size(); ++i) { auto const a{plan_of(Samples[i - 1].position)}; auto const b{plan_of(Samples[i].position)}; auto const ab{b - a}; auto const length2{glm::dot(ab, ab)}; auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; auto const distance{glm::length(point - (a + ab * t))}; if (distance < Distance) { Distance = distance; chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t; } } return chainage; } // the digits after the rotation make the number of the post, the last one the hectometre bool hectometres_of(std::vector const &Values, int &Hectometres) { if (Values.size() < 6) return false; Hectometres = 0; for (std::size_t i = 5; i < Values.size(); ++i) { double digit; if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0) return false; Hectometres = Hectometres * 10 + static_cast(digit); } return true; } std::string hekto_file(int const Hectometres, int const Style) { auto style{Style}; if (style > 2) { auto const pick{LocalRandom(0.0, 1.0)}; style = pick < 0.6 ? 0 : pick < 0.9 ? 1 : 2; } auto const size{Hectometres < 100 ? "1" : Hectometres < 1000 ? "10" : "100"}; return std::string{"przytorowe/slupek_hekto_"} + kHektoStyles[style] + "_" + size + ".inc"; } std::vector hekto_digits(int const Hectometres) { return {std::to_string((Hectometres / 1000) % 10), std::to_string((Hectometres / 100) % 10), std::to_string((Hectometres / 10) % 10), std::to_string(Hectometres % 10)}; } std::string km_text(int const Hectometres) { return format("%d.%d", Hectometres / 10, Hectometres % 10); } // points along the track from the end of a path on, the way a train goes: through a switch along its main track std::vector trace(TTrack &Track, int const Path, bool const Forward, double const Limit) { std::vector points; auto *track{&Track}; auto path{Path}; auto forward{Forward}; double length{0.0}; for (int guard = 0; guard < 64 && track != nullptr && length < Limit; ++guard) { bezier const curve{track->m_paths[path]}; auto const count{std::clamp(static_cast(curve.plan_length() / 1.0), 4, 400)}; for (int k = points.empty() ? 0 : 1; k <= count; ++k) { auto const point{curve.point(forward ? static_cast(k) / count : 1.0 - static_cast(k) / count)}; if (false == points.empty()) length += plan_distance(points.back(), point); points.push_back(point); } auto const exit{points.back()}; TTrack *next{nullptr}; int nextpath{0}; bool nextforward{true}; for (auto const &connection : editor_track::connected_points(*track, exit)) { auto *other{connection.first}; if (other == track || (other->iCategoryFlag & 15) != kRailCategory) continue; bool const start{connection.second.kind == editor_track::point_kind::start}; // entering a switch at its start the train takes the main track if (next == nullptr || (other == next && start && connection.second.path == 0)) { next = other; nextpath = connection.second.path; nextforward = start; } } track = next; path = nextpath; forward = nextforward; } return points; } // nearest point of the polyline glm::dvec3 nearest_on(std::vector const &Line, glm::dvec3 const &Point, double &Distance) { Distance = std::numeric_limits::max(); glm::dvec3 result{Point}; auto const point{plan_of(Point)}; for (std::size_t i = 1; i < Line.size(); ++i) { auto const a{plan_of(Line[i - 1])}; auto const ab{plan_of(Line[i]) - a}; auto const length2{glm::dot(ab, ab)}; auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; auto const distance{glm::length(point - (a + ab * t))}; if (distance < Distance) { Distance = distance; result = glm::mix(Line[i - 1], Line[i], t); } } return result; } void draw_label(ImDrawList *Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text) { auto const size{ImGui::CalcTextSize(Text.c_str())}; Draw->AddRectFilled(ImVec2(At.x + 8.0f, At.y - 8.0f), ImVec2(At.x + 14.0f + size.x, At.y - 6.0f + size.y), IM_COL32(0, 0, 0, 150), 3.0f); Draw->AddText(ImVec2(At.x + 11.0f, At.y - 7.0f), Colour, Text.c_str()); } } // namespace std::vector editor_mode::standing_templates(std::function const &Accept) const { std::vector result; for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) { if (false == scene::Layers.tracked(handle)) continue; auto const &included{scene::Layers.instance(handle)}; if (included.removed || included.dead || included.file == nullptr) continue; auto const file{normalized(*included.file)}; if (false == Accept(file) || included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) continue; std::string target; std::vector values; if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || values.size() < 5) continue; glm::dvec3 location; double yaw; if (false == numeric(values[1], location.x) || false == numeric(values[2], location.y) || false == numeric(values[3], location.z) || false == numeric(values[4], yaw)) continue; standing_template standing; standing.instance = handle; standing.file = file; standing.location = included.context.offset + location; standing.yaw = yaw; standing.values = std::move(values); result.push_back(std::move(standing)); } return result; } std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error) { if (Items.empty()) return 0; if (scene::Layers.empty()) { Error = STR_C("Templates can be placed only in a scenery opened for editing (-edit)"); return 0; } auto const layer{scene::Layers.resolve(scene::Layers.active())}; if (std::string reason; false == scene::Layers.accepts(layer, &reason)) { Error = STR_C("The active layer can't take them: ") + (reason.empty() ? std::string{STR_C("there's no active layer")} : reason); return 0; } auto const context{scene::Layers.layer(layer).context_insert()}; if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f}) { Error = STR_C("The file of the active layer would receive them with a rotation or a scale in effect, pick another layer"); return 0; } EditorSnapshot snap; for (auto const &item : Items) { auto const local{context.to_local(item.location)}; std::string directive; if (item.described) { include_info info; if (false == editor_includes::load(item.file, info, Error)) continue; directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast(item.yaw), item.track, item.tilt, item.values); } else { std::vector values{"none", number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)}; values.insert(values.end(), item.rest.begin(), item.rest.end()); directive = editor_includes::compose_directive(item.file, values); } auto const instance{scene::Layers.place(layer, item.file, directive)}; if (instance == 0) continue; simulation::State.preview_include(directive, context, layer, instance); snap.instances.push_back(instance); } if (snap.instances.empty()) { Error = STR_C("The active layer can't take them"); return 0; } auto const count{snap.instances.size()}; snap.node_name = Items.front().file; trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); return count; } std::size_t editor_mode::move_templates(std::vector> const &Moves) { EditorSnapshot snap; for (auto const &[standing, item] : Moves) { if (false == scene::Layers.tracked(standing.instance) || false == scene::Layers.removable(standing.instance)) continue; auto const &included{scene::Layers.instance(standing.instance)}; auto values{standing.values}; auto const local{item.location - included.context.offset}; values[1] = number(local.x, 3); values[2] = number(local.y, 3); values[3] = number(local.z, 3); values[4] = number(item.yaw, 2); auto before{scene::Layers.directive(standing.instance)}; set_include_directive(standing.instance, editor_includes::compose_directive(*included.file, values)); snap.directives.emplace_back(standing.instance, std::move(before)); } if (snap.directives.empty()) return 0; auto const count{snap.directives.size()}; snap.node_name = Moves.front().first.file; trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); return count; } std::size_t editor_mode::remove_templates(std::vector const &Instances) { EditorSnapshot snap; for (auto const instance : Instances) { if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) continue; scene::Layers.removed(instance, true); snap.instances.push_back(instance); } if (snap.instances.empty()) return 0; auto const count{snap.instances.size()}; trim_history(); m_history.push_back(std::move(snap)); g_redo.clear(); select_include(0); return count; } void editor_mode::hekto_start(TTrack &Track, glm::dvec3 const &Point) { auto &tool{m_hekto}; tool.track = &Track; tool.point = Point; tool.km_read = false; tool.dirty = true; tool.expand = true; tool.status.clear(); } void editor_mode::hekto_read_kilometrage() { auto &tool{m_hekto}; auto const &found{tool.found}; if (found.empty()) return; if (found.size() == 1) { // one post doesn't tell the way the kilometrage grows: it's taken to grow along the longer part of the line past the post auto const length{tool.samples.empty() ? 0.0 : tool.samples.back().chainage}; tool.growth = found.front().chainage > 0.5 * length ? -1 : 1; tool.km = found.front().hectometres / 10.0 + tool.growth * (tool.origin - found.front().chainage) / 1000.0; tool.status = format(STR_C("Kilometrage read from the post %s, check which way it grows"), km_text(found.front().hectometres).c_str()); return; } // a straight line through the numbers of the posts against where they stand double sc{0.0}, sk{0.0}, scc{0.0}, sck{0.0}; for (auto const &post : found) { auto const km{post.hectometres / 10.0}; sc += post.chainage; sk += km; scc += post.chainage * post.chainage; sck += post.chainage * km; } auto const n{static_cast(found.size())}; auto const denominator{n * scc - sc * sc}; if (std::abs(denominator) < 1e-9) return; auto const slope{(n * sck - sc * sk) / denominator}; auto const intercept{(sk - slope * sc) / n}; if (std::abs(std::abs(slope) * 1000.0 - 1.0) > 0.2) { tool.status = format(STR_C("The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them"), found.size()); return; } tool.growth = slope > 0.0 ? 1 : -1; tool.km = std::round((intercept + slope * tool.origin) * 1000.0) / 1000.0; tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size()); } void editor_mode::hekto_update() { auto &tool{m_hekto}; if (false == tool.dirty && tool.history == m_history.size()) return; tool.dirty = false; tool.history = m_history.size(); tool.posts.clear(); tool.found.clear(); tool.error.clear(); tool.samples.clear(); if (tool.track == nullptr || tool.track->m_editorremoved) { tool.track = nullptr; return; } tool.route = editor_track::run_route(*tool.track, tool.reach); if (tool.route.spans.empty()) { tool.error = STR_C("No line through this path"); return; } tool.samples = editor_track::sample_route(tool.route, kSampleStep); if (tool.samples.size() < 2) { tool.error = STR_C("No line through this path"); return; } double distance; tool.origin = project(tool.samples, tool.point, distance); auto const length{tool.samples.back().chainage}; for (auto const &standing : standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) { hekto_found post; post.post = standing; if (false == hectometres_of(standing.values, post.hectometres)) continue; post.chainage = project(tool.samples, standing.location, distance); if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length) continue; tool.found.push_back(post); } if (false == tool.km_read) { tool.km_read = true; hekto_read_kilometrage(); } auto const chainage_of = [&](int const Hectometres) { return tool.origin + tool.growth * (Hectometres / 10.0 - tool.km) * 1000.0; }; for (auto &post : tool.found) { post.expected = chainage_of(post.hectometres); post.error = post.chainage - post.expected; post.outside = true; } auto const km_at = [&](double const Chainage) { return tool.km + tool.growth * (Chainage - tool.origin) / 1000.0; }; auto const low{std::max(0, static_cast(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))}; auto const high{static_cast(std::floor(std::max(km_at(0.0), km_at(length)) * 10.0 + 1e-6))}; for (int hectometres = low; hectometres <= high && tool.posts.size() < 5000; ++hectometres) { hekto_post post; post.hectometres = hectometres; post.chainage = chainage_of(hectometres); if (post.chainage < 0.0 || post.chainage > length) continue; auto const axis{editor_track::sampled_position(tool.samples, post.chainage)}; auto const growing{sampled_direction(tool.samples, post.chainage) * static_cast(tool.growth)}; glm::dvec2 const left{-growing.y, growing.x}; // alternately: the even hectometres on the right, the odd ones on the left of the growing kilometrage auto const side{tool.side != 0 ? tool.side : (hectometres % 2 == 0 ? 1 : -1)}; auto const planar{plan_of(axis) + left * (side * tool.offset)}; post.position = placement_on_ground({planar.x, axis.y, planar.y}); post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); // of the posts with this number the one nearest to the place stands for it, the others are duplicates. it stands wrong // when it's farther from the place than allowed: along the line, across it, or on the other side auto best{std::numeric_limits::max()}; for (int i = 0; i < static_cast(tool.found.size()); ++i) { auto &found{tool.found[i]}; if (found.hectometres != hectometres) continue; found.outside = false; found.distance = plan_distance(found.post.location, post.position); if (found.distance < best) { best = found.distance; post.standing = i; } } for (int i = 0; i < static_cast(tool.found.size()); ++i) { auto &found{tool.found[i]}; if (found.hectometres != hectometres) continue; found.duplicate = i != post.standing; found.wrong = false == found.duplicate && found.distance > tool.tolerance; } tool.posts.push_back(post); } } void editor_mode::hekto_remove_duplicates() { auto &tool{m_hekto}; hekto_update(); std::vector instances; for (auto const &found : tool.found) if (found.duplicate) instances.push_back(found.post.instance); auto const removed{remove_templates(instances)}; tool.status = format(STR_P("%zu duplicate hectometre post removed, Ctrl+Z brings it back", "%zu duplicate hectometre posts removed, Ctrl+Z brings them back", static_cast(removed)), removed); WriteLog("Editor: " + tool.status); tool.dirty = true; } void editor_mode::hekto_place() { auto &tool{m_hekto}; hekto_update(); std::vector items; for (auto const &post : tool.posts) { if (post.standing >= 0) continue; template_item item; item.file = hekto_file(post.hectometres, tool.style); item.location = post.position; item.yaw = post.yaw; item.rest = hekto_digits(post.hectometres); items.push_back(std::move(item)); } std::string error; auto const placed{place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu hectometre post placed, Ctrl+Z takes it back", "%zu hectometre posts placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; if (placed > 0) first_step_done(first_step::posts); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } void editor_mode::hekto_fix() { auto &tool{m_hekto}; hekto_update(); std::vector> moves; for (auto const &post : tool.posts) { if (post.standing < 0 || false == tool.found[post.standing].wrong) continue; template_item item; item.location = post.position; item.yaw = post.yaw; moves.emplace_back(tool.found[post.standing].post, item); } auto const moved{move_templates(moves)}; tool.status = format(STR_P("%zu hectometre post moved to its place, Ctrl+Z takes it back", "%zu hectometre posts moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } void editor_mode::render_hekto_ui() { auto &tool{m_hekto}; auto *selected{selected_track()}; if (tool.track == nullptr || (selected != nullptr && selected != tool.track && ImGui::SmallButton(STR_C("Along the line of the selected path")))) { if (selected == nullptr) { ImGui::TextDisabled("%s", STR_C("Select a path of the line, or RMB on it: Hectometre posts")); return; } hekto_start(*selected, bezier{selected->m_paths.front()}.point(0.0)); } auto const changed = [&](bool const Changed) { if (Changed) tool.dirty = true; }; if (begin_fields("##hektokm", 0.45f)) { field_row("Kilometre at the point (km)"); changed(ImGui::InputDouble("##hektokilometre", &tool.km, 0.0, 0.0, "%.3f")); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); if (ImGui::Button(STR_C("Read from the posts"), ImVec2(-FLT_MIN, 0.0f))) { tool.km_read = false; tool.dirty = true; } ImGui::EndTable(); } ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); changed(segmented("growth", tool.growth, {{1, STR_C("along the line")}, {-1, STR_C("against it")}})); ImGui::TextDisabled("%s", STR_C("Side, looking where it grows")); changed(segmented("side", tool.side, {{1, STR_C("on the right")}, {-1, STR_C("on the left")}, {0, STR_C("alternately"), "The even hectometres on the right, the odd ones on the left"}})); if (ImGui::TreeNode(STR_C("More settings"))) { ImGui::TextDisabled("%s", STR_C("Look")); segmented("hektostyle", tool.style, {{0, STR_C("new")}, {1, STR_C("old")}, {2, STR_C("rotten")}, {3, STR_C("mixed"), "New, old and rotten ones at random, mostly new"}}); ImGui::TextDisabled("%s", STR_C("Turned by (deg)")); if (segmented("hektoturn", tool.turn, {{0, "0"}, {90, "90"}, {180, "180"}, {270, "270"}})) tool.dirty = true; if (begin_fields("##hektofields", 0.6f)) { field_row("From the axis (m)"); changed(ImGui::InputDouble("##hektooffset", &tool.offset, 0.0, 0.0, "%.2f")); field_row("Along the line each way (m)"); changed(ImGui::InputDouble("##hektoreach", &tool.reach, 0.0, 0.0, "%.0f")); field_row("Tolerance (m)"); changed(ImGui::InputDouble("##hektotolerance", &tool.tolerance, 0.0, 0.0, "%.1f")); item_tooltip("A post standing farther than this from its place counts as wrong"); ImGui::EndTable(); } ImGui::TreePop(); } tool.offset = std::clamp(tool.offset, 1.5, 30.0); tool.reach = std::clamp(tool.reach, 100.0, 100000.0); tool.tolerance = std::clamp(tool.tolerance, 0.5, 50.0); hekto_update(); if (false == tool.error.empty()) { ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); return; } auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.wrong; })}; auto const duplicates{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.duplicate; })}; ImGui::Text(STR_C("To place: %d"), static_cast(missing)); if (ImGui::IsItemHovered()) ImGui::SetTooltip(STR_C("Line %.0f m, km %s - %s. Standing: %zu, %d of them wrong"), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, tool.posts.empty() ? "-" : km_text(std::min(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), tool.found.size(), static_cast(wrong)); if (missing > 0) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); if (ImGui::Button(format(STR_P("Place %d post", "Place %d posts", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, std::round(ImGui::GetFrameHeight() * 1.3f)))) hekto_place(); ImGui::PopStyleColor(2); } if (duplicates > 0) { if (ImGui::Button(format(STR_P("Remove %d duplicate post", "Remove %d duplicate posts", static_cast(duplicates)), static_cast(duplicates)).c_str())) hekto_remove_duplicates(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("Posts with a number another post nearer to its place already has")); } if (wrong > 0 || duplicates > 0) { if (wrong > 0) { if (duplicates > 0) ImGui::SameLine(); if (ImGui::Button(format(STR_P("Move %d wrong post to its place", "Move %d wrong posts to their places", static_cast(wrong)), static_cast(wrong)).c_str())) hekto_fix(); } ImGui::BeginChild("##hektowrong", ImVec2(0.0f, std::min(6, static_cast(wrong + duplicates)) * ImGui::GetTextLineHeightWithSpacing() + 6.0f), true); for (auto const &post : tool.found) { if (false == post.wrong && false == post.duplicate) continue; auto const label{post.duplicate ? format(STR_C("km %s duplicate##%u"), km_text(post.hectometres).c_str(), post.post.instance) : format(STR_C("km %s stands %.1f m from its place##%u"), km_text(post.hectometres).c_str(), post.distance, post.post.instance)}; if (ImGui::Selectable(label.c_str())) { fly_to(post.post.location, glm::normalize(glm::dvec3{-1.0, -0.8, -1.0}), 20.0); } } ImGui::EndChild(); } if (false == tool.status.empty()) ImGui::TextWrapped("%s", tool.status.c_str()); } void editor_mode::draw_hekto_overlay() const { auto const &tool{m_hekto}; if (false == tool.open || tool.track == nullptr) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; for (auto const &post : tool.posts) { if (glm::distance(post.position, camera) > kOverlayRange) continue; ImVec2 screen; if (false == projection.project(post.position, screen)) continue; bool const standing{post.standing >= 0 && false == tool.found[post.standing].wrong}; auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)}; drawlist->AddCircleFilled(screen, 4.0f, colour); draw_label(drawlist, screen, colour, km_text(post.hectometres)); } for (auto const &post : tool.found) { if ((false == post.wrong && false == post.duplicate) || glm::distance(post.post.location, camera) > kOverlayRange) continue; ImVec2 screen; if (false == projection.project(post.post.location, screen)) continue; auto const colour{post.duplicate ? IM_COL32(255, 160, 40, 235) : IM_COL32(255, 70, 60, 235)}; drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f); draw_label(drawlist, screen, colour, post.duplicate ? format(STR_C("km %s duplicate"), km_text(post.hectometres).c_str()) : format(STR_C("km %s, %.1f m from its place"), km_text(post.hectometres).c_str(), post.distance)); if (post.duplicate) continue; for (auto const &target : tool.posts) if (target.hectometres == post.hectometres) projection.line(drawlist, post.post.location, target.position, IM_COL32(255, 70, 60, 160), 1.5f); } ImVec2 screen; if (projection.project(tool.point, screen)) { drawlist->AddCircle(screen, 7.0f, IM_COL32(90, 200, 255, 235), 16, 2.5f); draw_label(drawlist, screen, IM_COL32(90, 200, 255, 235), format("km %.3f", tool.km)); } } void editor_mode::fouling_update() { auto &tool{m_fouling}; auto *selected{selected_track()}; if (tool.scope == 0 && selected != tool.selected) tool.dirty = true; if (false == tool.dirty && tool.history == m_history.size()) return; tool.dirty = false; tool.history = m_history.size(); tool.selected = selected; tool.points.clear(); tool.found.clear(); tool.error.clear(); std::vector switches; glm::dvec3 const camera{Global.pCamera.Pos}; if (tool.scope == 0) { if (selected == nullptr || selected->eType != tt_Switch) { tool.error = STR_C("Select a switch, or check around the camera"); return; } switches.push_back(selected); } else { for (auto *track : simulation::Paths.sequence()) if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved && (track->iCategoryFlag & 15) == kRailCategory && track->m_paths.size() >= 2 && (tool.scope == 2 || glm::distance(track->location(), camera) <= tool.reach)) switches.push_back(track); } for (auto *track : switches) { if (track->m_paths.size() < 2 || false == editor_track::is_supported(*track)) continue; auto const main{trace(*track, 0, true, kFoulingTrace)}; auto const branch{trace(*track, 1, true, kFoulingTrace)}; if (main.size() < 2 || branch.size() < 2) continue; glm::dvec3 previous{branch.front()}; double previousdistance{0.0}; for (std::size_t i = 1; i < branch.size(); ++i) { double distance; nearest_on(main, branch[i], distance); if (distance < tool.spacing) { previous = branch[i]; previousdistance = distance; continue; } auto const f{distance - previousdistance > 1e-9 ? (tool.spacing - previousdistance) / (distance - previousdistance) : 1.0}; auto const onbranch{glm::mix(previous, branch[i], std::clamp(f, 0.0, 1.0))}; double unused; auto const onmain{nearest_on(main, onbranch, unused)}; fouling_point point; point.track = track; auto const middle{(onbranch + onmain) * 0.5}; // the points of the paths are at the rail foot, the heads of the rails are the height of the rail over them point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z}); auto const back{plan_of(previous - branch[i])}; point.yaw = std::fmod(heading_of(glm::length(back) > 1e-9 ? glm::normalize(back) : glm::dvec2{0.0, 1.0}) + tool.turn, 360.0); tool.points.push_back(point); break; } } // markers standing: each goes to the nearest fouling point, the others are left alone auto const markers{standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })}; for (auto const &marker : markers) { int nearest{-1}; double best{std::numeric_limits::max()}; for (int i = 0; i < static_cast(tool.points.size()); ++i) { auto const distance{plan_distance(marker.location, tool.points[i].position)}; if (distance < best) { best = distance; nearest = i; } } if (nearest < 0 || best > 40.0) continue; auto &point{tool.points[nearest]}; if (point.standing >= 0 && tool.found[point.standing].error <= best) continue; fouling_found found; found.marker = marker; found.point = nearest; found.error = best; found.height_error = marker.location.y - point.position.y; found.wrong = best > tool.tolerance || (tool.height_mode == 1 && std::abs(found.height_error) > tool.height_tolerance); if (point.standing >= 0) tool.found[point.standing] = found; else { point.standing = static_cast(tool.found.size()); tool.found.push_back(found); } } auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; tool.status = format(STR_C("%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong"), switches.size(), tool.points.size(), static_cast(missing), static_cast(wrong)); } void editor_mode::fouling_place(int const Only) { auto &tool{m_fouling}; fouling_update(); std::vector items; for (int i = 0; i < static_cast(tool.points.size()); ++i) { auto const &point{tool.points[i]}; if (point.standing >= 0 || (Only >= 0 && i != Only)) continue; template_item item; item.file = kFoulingModels[std::clamp(tool.model, 0, 3)]; item.location = point.position; item.yaw = point.yaw; if (tool.model != 2) item.rest = {"0"}; items.push_back(std::move(item)); } std::string error; auto const placed{place_templates(items, error)}; tool.status = placed > 0 ? format(STR_P("%zu fouling point marker placed, Ctrl+Z takes it back", "%zu fouling point markers placed, Ctrl+Z takes them back", static_cast(placed)), placed) : error; WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } void editor_mode::fouling_fix(int const Only) { auto &tool{m_fouling}; fouling_update(); std::vector> moves; for (int i = 0; i < static_cast(tool.found.size()); ++i) { auto const &found{tool.found[i]}; if (false == found.wrong || (Only >= 0 && i != Only)) continue; template_item item; auto const &point{tool.points[found.point]}; if (found.error <= tool.tolerance) { // in its place, only too high or too low item.location = {found.marker.location.x, point.position.y, found.marker.location.z}; item.yaw = found.marker.yaw; } else { item.location = point.position; item.yaw = point.yaw; } moves.emplace_back(found.marker, item); } auto const moved{move_templates(moves)}; tool.status = format(STR_P("%zu fouling point marker moved to its place, Ctrl+Z takes it back", "%zu fouling point markers moved to their places, Ctrl+Z takes it back", static_cast(moved)), moved); WriteLog("Editor: " + tool.status, logtype::generic); tool.dirty = true; } std::optional> editor_mode::fouling_hit() const { auto const &tool{m_fouling}; screen_projection const projection; auto const &mouse{ImGui::GetIO().MousePos}; glm::dvec3 const camera{Global.pCamera.Pos}; std::optional> result; auto best{kFoulingPick * kFoulingPick}; auto const consider = [&](glm::dvec3 const &Position, int const Index, bool const Marker) { ImVec2 screen; if (glm::distance(Position, camera) > kOverlayRange || false == projection.project(Position, screen)) return; auto const distance2{(screen.x - mouse.x) * (screen.x - mouse.x) + (screen.y - mouse.y) * (screen.y - mouse.y)}; if (distance2 < best) { best = distance2; result = std::make_pair(Index, Marker); } }; for (int i = 0; i < static_cast(tool.points.size()); ++i) if (tool.points[i].standing < 0) consider(tool.points[i].position, i, false); for (int i = 0; i < static_cast(tool.found.size()); ++i) if (tool.found[i].wrong) consider(tool.found[i].marker.location, i, true); return result; } void editor_mode::render_fouling_ui() { auto &tool{m_fouling}; auto const changed = [&](bool const Changed) { if (Changed) tool.dirty = true; }; changed(segmented("foulingscope", tool.scope, {{0, STR_C("The selected switch")}, {1, STR_C("Around the camera")}, {2, STR_C("Whole scenery")}})); fouling_update(); if (false == tool.error.empty()) ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", tool.error.c_str()); else { ImGui::TextWrapped("%s", tool.status.c_str()); auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; auto const tall{std::round(ImGui::GetFrameHeight() * 1.3f)}; if (missing > 0) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); if (ImGui::Button(format(STR_P("Place %d marker", "Place %d markers", static_cast(missing)), static_cast(missing)).c_str(), ImVec2(-1.0f, tall))) fouling_place(); ImGui::PopStyleColor(2); } if (wrong > 0) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.4f, 0.1f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.48f, 0.15f, 1.0f)); if (ImGui::Button(format(STR_P("Move %d wrong marker to its place", "Move %d wrong markers to their places", static_cast(wrong)), static_cast(wrong)).c_str(), ImVec2(-1.0f, tall))) fouling_fix(); ImGui::PopStyleColor(2); } if (missing + wrong > 0) { // each one: a click flies there, the button fixes it alone; in the view a click on its point does the same ImGui::TextDisabled("%s", STR_C("Click the green or red point in the view: that one only")); ImGui::BeginChild("##foulinglist", ImVec2(0.0f, std::min(8, static_cast(missing + wrong)) * ImGui::GetFrameHeightWithSpacing() + 6.0f), true); auto const look{glm::normalize(glm::dvec3{-1.0, -0.8, -1.0})}; for (int i = 0; i < static_cast(tool.points.size()); ++i) { auto const &point{tool.points[i]}; if (point.standing >= 0) continue; ImGui::PushID(i); if (ImGui::SmallButton(STR_C("Place"))) fouling_place(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: no W17"), point.track->name().c_str()).c_str())) fly_to(point.position, look, 25.0); ImGui::PopID(); } for (int i = 0; i < static_cast(tool.found.size()); ++i) { auto const &found{tool.found[i]}; if (false == found.wrong) continue; ImGui::PushID(1000000 + i); if (ImGui::SmallButton(STR_C("Fix"))) fouling_fix(i); ImGui::SameLine(); if (ImGui::Selectable(format(STR_C("%s: W17 %.1f m off, %+.2f m high"), tool.points[found.point].track->name().c_str(), found.error, found.height_error).c_str())) fly_to(found.marker.location, look, 25.0); ImGui::PopID(); } ImGui::EndChild(); } } if (ImGui::TreeNode(STR_C("More settings"))) { if (begin_fields("##foulingfields", 0.6f)) { if (tool.scope == 1) { field_row("Around the camera (m)"); changed(ImGui::InputDouble("##foulingreach", &tool.reach, 0.0, 0.0, "%.0f")); } field_row("Spacing of the axes (m)"); changed(ImGui::InputDouble("##foulingspacing", &tool.spacing, 0.0, 0.0, "%.2f")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s\n%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them"), STR_C("3.5 m on the straight track; more on the curves, where the cars lean out")); field_row("Tolerance (m)"); changed(ImGui::InputDouble("##foulingtolerance", &tool.tolerance, 0.0, 0.0, "%.1f")); item_tooltip("A marker standing farther than this from its place counts as wrong"); ImGui::EndTable(); } changed(segmented("foulingheight", tool.height_mode, {{0, STR_C("On the ground")}, {1, STR_C("Over the rail heads")}})); if (tool.height_mode == 1 && begin_fields("##foulingheights", 0.6f)) { field_row("by (m)"); changed(ImGui::InputDouble("##foulingby", &tool.height, 0.0, 0.0, "%.3f")); field_row("Height tolerance (m)"); changed(ImGui::InputDouble("##foulingheighttolerance", &tool.height_tolerance, 0.0, 0.0, "%.3f")); item_tooltip("A marker higher or lower than its place by more than this counts as wrong"); tool.height_tolerance = std::clamp(tool.height_tolerance, 0.005, 1.0); ImGui::EndTable(); } ImGui::TextDisabled("%s", STR_C("Marker")); segmented("foulingmodel", tool.model, {{0, "w17", "Round post (ip/pkp/w17.inc)"}, {1, "w17_new", "Round post, taller than w17 (ip/pkp/w17_new.inc)"}, {2, "w17_old", "Square post (ip/pkp/w17_old.inc)"}, {3, STR_C("I-beam"), "Post of an HEB I-beam (ip/pkp/w17_dwuteo.inc)"}}); ImGui::TextDisabled("%s", STR_C("Turned by (deg)")); if (segmented("foulingturn", tool.turn, {{0, "0"}, {90, "90"}, {180, "180"}, {270, "270"}})) tool.dirty = true; ImGui::TreePop(); } tool.reach = std::clamp(tool.reach, 50.0, 50000.0); tool.spacing = std::clamp(tool.spacing, 2.0, 10.0); tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0); } void editor_mode::draw_fouling_overlay() const { auto const &tool{m_fouling}; if (false == tool.open) return; screen_projection const projection; auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; glm::dvec3 const camera{Global.pCamera.Pos}; for (auto const &point : tool.points) { if (glm::distance(point.position, camera) > kOverlayRange) continue; ImVec2 screen; if (false == projection.project(point.position, screen)) continue; bool const standing{point.standing >= 0 && false == tool.found[point.standing].wrong}; auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)}; drawlist->AddCircleFilled(screen, 4.0f, colour); draw_label(drawlist, screen, colour, "W17"); } for (auto const &found : tool.found) { if (false == found.wrong || glm::distance(found.marker.location, camera) > kOverlayRange) continue; ImVec2 screen; if (false == projection.project(found.marker.location, screen)) continue; drawlist->AddCircle(screen, 9.0f, IM_COL32(255, 70, 60, 235), 16, 2.5f); draw_label(drawlist, screen, IM_COL32(255, 70, 60, 235), format(STR_C("W17 %.1f m off, %+.2f m high"), found.error, found.height_error)); projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f); } }