From 435b80eb558933dbbb95409c3694265e59d3daad Mon Sep 17 00:00:00 2001 From: maj00r Date: Thu, 22 Oct 2026 14:33:18 +0200 Subject: [PATCH] feat(editor): the hectometre posts go by the kilometrage of the line where the Kilometrage tool sets it, its jumps too, a post at a jump of the km on from it or of the km it is reached at, the km given at the point still there as a choice; the posts standing go with the place of their number nearest to them --- application/editormodekm.cpp | 55 ++------- application/editormodelineside.cpp | 175 ++++++++++++++++++++--------- application/editormodelineside.h | 5 + editor/editorKilometrage.cpp | 80 +++++++++++++ editor/editorKilometrage.hpp | 14 +++ lang/editor_pl.po | 12 ++ 6 files changed, 240 insertions(+), 101 deletions(-) diff --git a/application/editormodekm.cpp b/application/editormodekm.cpp index c851fb0d..d1fe8362 100644 --- a/application/editormodekm.cpp +++ b/application/editormodekm.cpp @@ -163,51 +163,22 @@ void km_tool::refresh() auto const high{axis->anchors.back().chainage + 1.0}; for (auto const chainage : axis->chainage) entry.guessed.push_back(chainage < low || chainage > high ? 1 : 0); - std::vector jumps; for (auto const &anchor : axis->anchors) - { - if (std::abs(anchor.after - anchor.before) < 1e-6) - continue; - jumps.push_back(anchor.chainage); - auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; - entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); - } - // a tick at each hectometre the km pass along the axis, none at a jump - auto const hectometres_along = [&](std::vector const &Points, std::vector const &Km, std::vector const &Chainage, std::vector &Ticks) { - for (std::size_t i = 1; i < Points.size(); ++i) + if (std::abs(anchor.after - anchor.before) >= 1e-6) { - auto const from{std::min(Chainage[i - 1], Chainage[i])}; - auto const to{std::max(Chainage[i - 1], Chainage[i])}; - if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; })) - continue; - auto const k0{Km[i - 1]}; - auto const k1{Km[i]}; - if (std::abs(k1 - k0) < 1e-9) - continue; - auto way{plan_of(Points[i] - Points[i - 1])}; - way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0}; - auto const first{static_cast(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1}; - auto const last{static_cast(std::floor(std::max(k0, k1) * 10.0 + 1e-9))}; - for (auto hectometres = first; hectometres <= last && hectometres < first + 10; ++hectometres) - Ticks.push_back({glm::mix(Points[i - 1], Points[i], (hectometres / 10.0 - k0) / (k1 - k0)), {-way.y, 0.0, way.x}, hectometres, false}); + auto const way{kilometrage::direction_on(*axis, anchor.chainage)}; + entry.ticks.push_back({kilometrage::point_on(*axis, anchor.chainage), {-way.y, 0.0, way.x}, 0, true}); } - }; - std::vector km; - for (auto const chainage : axis->chainage) - km.push_back(kilometrage::km_on(*axis, chainage)); - hectometres_along(axis->points, km, axis->chainage, entry.ticks); + for (auto const &hectometre : kilometrage::hectometres(*axis)) + entry.ticks.push_back({hectometre.at, {-hectometre.rising.y, 0.0, hectometre.rising.x}, hectometre.number, false}); m_drawn.push_back(std::move(entry)); - // the shorter track where the tracks run apart, with its irregular hectometre + // the shorter track where the tracks run apart for (auto const &side : axis->sides) { drawn apart; apart.line = line; apart.points = side.points; apart.guessed.assign(side.points.size(), 0); - km.clear(); - for (auto const along : side.along) - km.push_back(kilometrage::km_on(*axis, side, along)); - hectometres_along(side.points, km, side.chainage, apart.ticks); m_drawn.push_back(std::move(apart)); } } @@ -303,18 +274,8 @@ void km_tool::pick(TTrack &Track, glm::dvec3 const &Point) else { // the line the point lies on, and its km there - auto best{kReadoutReach}; - for (auto const &line : kilometrage::lines()) - if (auto const *axis{kilometrage::axis_of(line)}; axis != nullptr) - { - double gap{0.0}; - kilometrage::chainage_at(*axis, entry.point, gap); - if (gap < best) - { - best = gap; - m_line = line; - } - } + if (auto const line{kilometrage::line_at(entry.point, kReadoutReach)}; false == line.empty()) + m_line = line; double km{0.0}; if (false == m_line.empty() && kilometrage::km_at(m_line, entry.point, km, kReadoutReach)) { diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp index 906d419a..5a5ab50f 100644 --- a/application/editormodelineside.cpp +++ b/application/editormodelineside.cpp @@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorFormat.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" +#include "editor/editorKilometrage.hpp" #include "scene/scene.h" #include "scene/scenelayers.h" @@ -442,61 +443,97 @@ void hekto_tool::hekto_update() continue; tool.found.push_back(post); } - if (false == tool.km_read) + tool.line_set = kilometrage::line_at(tool.point); + tool.line = tool.own_km ? std::string{} : tool.line_set; + if (tool.line.empty() && 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)}; + auto const place = [&](hekto_post &Post) { + auto const axis{editor_track::sampled_position(tool.samples, Post.chainage)}; + auto const growing{sampled_direction(tool.samples, Post.chainage) * static_cast(Post.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 side{tool.side != 0 ? tool.side : (Post.hectometres % 2 == 0 ? 1 : -1)}; auto const planar{plan_of(axis) + left * (side * tool.offset)}; - post.position = m_ground.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) + Post.position = m_ground.placement_on_ground({planar.x, axis.y, planar.y}); + Post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); + tool.posts.push_back(Post); + }; + auto const *axis{tool.line.empty() ? nullptr : kilometrage::axis_of(tool.line)}; + if (axis != nullptr) + { + // the hectometres of the kilometrage of the line, the jumps in it too + double km{0.0}; + if (kilometrage::km_at(tool.line, tool.point, km)) + tool.km = std::round(km * 1000.0) / 1000.0; + tool.growth = kilometrage::growth_at(tool.line, tool.point, sampled_direction(tool.samples, tool.origin)) < 0 ? -1 : 1; + for (auto const &hectometre : kilometrage::hectometres(*axis)) { - auto &found{tool.found[i]}; - if (found.hectometres != hectometres) + hekto_post post; + post.hectometres = hectometre.number; + post.chainage = project(tool.samples, hectometre.at, distance); + if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length || hectometre.number < 0 || tool.posts.size() >= 5000) continue; - found.outside = false; - found.distance = plan_distance(found.post.location, post.position); - if (found.distance < best) + post.growth = glm::dot(hectometre.rising, sampled_direction(tool.samples, post.chainage)) < 0.0 ? -1 : 1; + place(post); + } + std::sort(tool.posts.begin(), tool.posts.end(), [](hekto_post const &A, hekto_post const &B) { return A.chainage < B.chainage; }); + } + else + { + 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 = tool.origin + tool.growth * (hectometres / 10.0 - tool.km) * 1000.0; + post.growth = tool.growth; + if (post.chainage < 0.0 || post.chainage > length) + continue; + place(post); + } + } + // each post standing goes with the place of its number nearest to it; of the posts going with a place the one nearest to it + // 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 + for (auto &found : tool.found) + { + found.outside = true; + found.place = -1; + for (int i = 0; i < static_cast(tool.posts.size()); ++i) + { + auto const &post{tool.posts[i]}; + if (post.hectometres != found.hectometres) + continue; + auto const distance{plan_distance(found.post.location, post.position)}; + if (found.place < 0 || distance < found.distance) { - best = found.distance; - post.standing = i; + found.outside = false; + found.place = i; + found.distance = distance; + found.expected = post.chainage; + found.error = found.chainage - post.chainage; } } - 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); + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto const &found{tool.found[i]}; + if (found.place < 0) + continue; + auto &post{tool.posts[found.place]}; + if (post.standing < 0 || found.distance < tool.found[post.standing].distance) + post.standing = i; + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + found.duplicate = found.place >= 0 && tool.posts[found.place].standing != i; + found.wrong = found.place >= 0 && false == found.duplicate && found.distance > tool.tolerance; } } @@ -576,21 +613,40 @@ void hekto_tool::render_options() if (Changed) tool.dirty = true; }; - if (begin_fields("##hektokm", 0.45f)) + if (false == tool.line.empty()) { - 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))) + ImGui::TextWrapped("%s", format(STR_C("The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point"), tool.line.c_str(), kilometrage::text(tool.km).c_str()).c_str()); + if (ImGui::SmallButton(STR_C("Give the km here instead"))) { + tool.own_km = true; tool.km_read = false; tool.dirty = true; } - ImGui::EndTable(); + item_tooltip("The posts go by the km given at the point and the way it grows, without the jumps of the line"); + } + else + { + if (false == tool.line_set.empty() && ImGui::SmallButton(format(STR_C("Follow the km of line %s"), tool.line_set.c_str()).c_str())) + { + tool.own_km = false; + 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("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"))) @@ -1056,6 +1112,7 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist auto &tool{m_hekto}; if (values.count("reach")) tool.reach = std::stod(values["reach"]); + tool.own_km = values["own"] == "1"; tool.dirty = true; hekto_update(); if (values.count("km")) @@ -1067,7 +1124,10 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist tool.dirty = true; hekto_update(); auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; - WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), tool.status.c_str())); + 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; })}; + WriteLog(format("SELFTEST hekto: line %s km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing, %d wrong, %d duplicate. %s", tool.line.empty() ? "-" : tool.line.c_str(), tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), + static_cast(wrong), static_cast(duplicates), tool.status.c_str())); for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) { auto const &post{tool.posts[i]}; @@ -1076,10 +1136,17 @@ editor_selftest_result hekto_tool::selftest(std::string const &Command, std::ist auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis}; glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z}; lateral = glm::length(offset); - auto const right{(direction.x * offset.y - direction.z * offset.x) * tool.growth >= 0.0}; + auto const right{(direction.x * offset.y - direction.z * offset.x) * post.growth >= 0.0}; WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y, post.position.z)); } + if (values["list"] == "1") + { + std::string list; + for (auto const &post : tool.posts) + list += format(" %d@%.0f", post.hectometres, post.chainage); + WriteLog("SELFTEST hekto posts:" + list); + } if (values["place"] == "1") { hekto_place(); diff --git a/application/editormodelineside.h b/application/editormodelineside.h index d7b23606..1ace4a1c 100644 --- a/application/editormodelineside.h +++ b/application/editormodelineside.h @@ -41,6 +41,7 @@ class hekto_tool final : public editor_tool double chainage{0.0}; glm::dvec3 position{0.0}; double yaw{0.0}; + int growth{1}; // 1: the kilometrage grows along the line there, -1: against int standing{-1}; // post already there with this number }; struct hekto_found @@ -54,6 +55,7 @@ class hekto_tool final : public editor_tool bool wrong{false}; bool duplicate{false}; // another post with the same number stands nearer to its place bool outside{false}; // its number belongs past the ends of the line + int place{-1}; // of the posts, the one with its number nearest to it }; struct hekto_state { @@ -62,6 +64,9 @@ class hekto_tool final : public editor_tool double km{0.0}; bool km_read{false}; // the kilometrage was read from the posts already standing int growth{1}; // 1: grows along the line, -1: against + std::string line; // of the kilometrage set for the line in the Kilometrage tool the posts follow, empty for the one given here + std::string line_set; // the line the Kilometrage tool sets the kilometrage of at the point, empty for none + bool own_km{false}; // the kilometrage given here, even where the line has one set int side{1}; // 1: to the right of the growing kilometrage, -1: to the left, 0: alternately double offset{3.0}; // m from the axis int style{0}; // nowy, stary, zgnity, mixed diff --git a/editor/editorKilometrage.cpp b/editor/editorKilometrage.cpp index 860612ff..9f527dd2 100644 --- a/editor/editorKilometrage.cpp +++ b/editor/editorKilometrage.cpp @@ -927,6 +927,86 @@ bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double return true; } +std::string line_at(glm::dvec3 const &Point, double const Reach) +{ + std::string result; + auto nearest{Reach}; + for (auto const &line : lines()) + if (auto const *found{axis_of(line)}; found != nullptr) + { + double gap{0.0}; + chainage_at(*found, Point, gap); + if (gap < nearest) + { + nearest = gap; + result = line; + } + } + return result; +} + +int growth_at(std::string const &Line, glm::dvec3 const &Point, glm::dvec2 const &Way, double const Reach) +{ + auto const *found{axis_of(Line)}; + if (found == nullptr) + return 0; + double gap{0.0}; + auto const chainage{chainage_at(*found, Point, gap)}; + if (gap > Reach) + return 0; + return glm::dot(direction_on(*found, chainage) * static_cast(found->growth), Way) >= 0.0 ? 1 : -1; +} + +std::vector hectometres(axis const &Axis) +{ + std::vector result; + std::vector jumps; + for (auto const &anchor : Axis.anchors) + if (std::abs(anchor.after - anchor.before) >= 1e-6) + jumps.push_back(anchor.chainage); + auto const along = [&](std::vector const &Points, std::vector const &Km, std::vector const &Chainage) { + for (std::size_t i = 1; i < Points.size(); ++i) + { + auto const from{std::min(Chainage[i - 1], Chainage[i])}; + auto const to{std::max(Chainage[i - 1], Chainage[i])}; + if (std::any_of(jumps.begin(), jumps.end(), [&](double const Jump) { return Jump >= from - 1e-6 && Jump <= to + 1e-6; })) + continue; + auto const k0{Km[i - 1]}; + auto const k1{Km[i]}; + if (std::abs(k1 - k0) < 1e-9) + continue; + auto way{plan_of(Points[i] - Points[i - 1])}; + way = glm::length(way) > 1e-9 ? glm::normalize(way) : glm::dvec2{0.0, 1.0}; + auto const first{static_cast(std::floor(std::min(k0, k1) * 10.0 + 1e-9)) + 1}; + auto const last{static_cast(std::floor(std::max(k0, k1) * 10.0 + 1e-9))}; + for (auto number = first; number <= last && number < first + 10; ++number) + result.push_back({glm::mix(Points[i - 1], Points[i], (number / 10.0 - k0) / (k1 - k0)), k1 > k0 ? way : -way, number}); + } + }; + // at a jump the post of the km on from it, as the posts of a line built anew begin there, else of the km the jump is reached at + for (auto const &anchor : Axis.anchors) + { + if (std::abs(anchor.after - anchor.before) < 1e-6) + continue; + auto const round = [](double const Km) { return std::abs(Km * 10.0 - std::round(Km * 10.0)) < 1e-4; }; + auto const km{round(anchor.after) ? anchor.after : anchor.before}; + if (round(km)) + result.push_back({point_on(Axis, anchor.chainage), direction_on(Axis, anchor.chainage) * static_cast(Axis.growth), static_cast(std::lround(km * 10.0))}); + } + std::vector km; + for (auto const chainage : Axis.chainage) + km.push_back(km_on(Axis, chainage)); + along(Axis.points, km, Axis.chainage); + for (auto const &side : Axis.sides) + { + km.clear(); + for (auto const distance : side.along) + km.push_back(km_on(Axis, side, distance)); + along(side.points, km, side.chainage); + } + return result; +} + TTrack *track_near(glm::dvec3 const &Point, double const Reach) { TTrack *best{nullptr}; diff --git a/editor/editorKilometrage.hpp b/editor/editorKilometrage.hpp index d266f6ee..2005a7a6 100644 --- a/editor/editorKilometrage.hpp +++ b/editor/editorKilometrage.hpp @@ -127,6 +127,20 @@ glm::dvec2 direction_on(axis const &Axis, double const Chainage); double km_on(axis const &Axis, axis::side const &Side, double const Along); // the km of the line at the point, when it lies within the reach of the axis bool km_at(std::string const &Line, glm::dvec3 const &Point, double &Km, double const Reach = 30.0); +// the line whose axis, or a track running apart from it, passes nearest to the point within the reach, empty for none +std::string line_at(glm::dvec3 const &Point, double const Reach = 30.0); +// the km of the line rise along the way at the point: 1, against it: -1, 0 where the axis doesn't reach +int growth_at(std::string const &Line, glm::dvec3 const &Point, glm::dvec2 const &Way, double const Reach = 30.0); +// a hectometre the km of a line pass +struct hectometre +{ + glm::dvec3 at{0.0}; + glm::dvec2 rising{0.0, 1.0}; // the way the km rise there, in the plan + int number{0}; // of the hectometres from km 0, 1043 for km 104,3 +}; +// the hectometres along the axis and along the tracks running apart from it; at a jump the one of the km on from it, else of +// the km it is reached at, whichever is a whole hectometre +std::vector hectometres(axis const &Axis); // the track nearest to the point in the plan within the reach, nullptr for none TTrack *track_near(glm::dvec3 const &Point, double const Reach); // the path of the track nearest to the point in the plan, On: the point of it nearest diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 3a2baf9a..0acac6d8 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -10780,3 +10780,15 @@ msgstr "Przez stację linia biegnie tym torem, a nie po prostych torach rozjazd msgid "%s: this way" msgstr "%s: tędy" +msgid "The km of line %s, as the Kilometrage tool sets them, its jumps too: km %s at the point" +msgstr "Kilometraż linii %s z narzędzia Kilometraż, razem z uskokami: km %s w punkcie" + +msgid "Give the km here instead" +msgstr "Podaj kilometr tutaj" + +msgid "The posts go by the km given at the point and the way it grows, without the jumps of the line" +msgstr "Słupki pójdą według kilometra podanego w punkcie i kierunku, w którym rośnie, bez uskoków linii" + +msgid "Follow the km of line %s" +msgstr "Według kilometrażu linii %s" +