17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00

feat(editor): recognize collinear straights, measure

This commit is contained in:
maj00r
2026-10-02 22:42:09 +02:00
parent d6519531ed
commit b19f3de0d4
5 changed files with 319 additions and 1 deletions

View File

@@ -1013,6 +1013,8 @@ bool editor_mode::update()
draw_track_overlay(); draw_track_overlay();
if (route_active()) if (route_active())
draw_route_overlay(); draw_route_overlay();
if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights)
draw_straights_overlay();
// --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) ---
if (ui()->mode() == nodebank_panel::FILL) if (ui()->mode() == nodebank_panel::FILL)

View File

@@ -319,6 +319,25 @@ class editor_mode : public application_mode
glm::dvec3 route_vertex_position(int const Vertex) const; glm::dvec3 route_vertex_position(int const Vertex) const;
bool route_active() const; bool route_active() const;
route_design m_route; route_design m_route;
struct straights_state
{
editor_track::straight_tolerance tolerance;
double minimum_length{50.0};
std::vector<editor_track::straight> found;
int listed{-1};
TTrack *current_for{nullptr};
editor_track::straight current;
editor_track::straight a;
editor_track::straight b;
char filter[32]{};
};
void render_straights_ui();
void draw_straights_overlay() const;
editor_track::straight const &current_straight();
straights_state m_straights;
enum class track_tab { straights, route, path };
track_tab m_track_tab{track_tab::straights};
bool m_route_tab{true}; bool m_route_tab{true};
bool m_route_gizmo_using{false}; bool m_route_gizmo_using{false};
glm::mat4 m_route_gizmo{1.0f}; glm::mat4 m_route_gizmo{1.0f};

View File

@@ -423,14 +423,23 @@ void editor_mode::render_track_ui()
{ {
if (ImGui::BeginTabBar("##trackediting")) if (ImGui::BeginTabBar("##trackediting"))
{ {
if (ImGui::BeginTabItem("Straights"))
{
m_track_tab = track_tab::straights;
m_route_tab = true;
render_straights_ui();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Route design")) if (ImGui::BeginTabItem("Route design"))
{ {
m_track_tab = track_tab::route;
m_route_tab = true; m_route_tab = true;
render_route_ui(); render_route_ui();
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem("Single path")) if (ImGui::BeginTabItem("Single path"))
{ {
m_track_tab = track_tab::path;
m_route_tab = false; m_route_tab = false;
render_path_ui(); render_path_ui();
ImGui::EndTabItem(); ImGui::EndTabItem();
@@ -632,7 +641,7 @@ void editor_mode::render_path_ui()
bool editor_mode::route_active() const bool editor_mode::route_active() const
{ {
return ui()->mode() == nodebank_panel::TRACK && m_route_tab && false == m_route.chain.tracks.empty(); return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty();
} }
void editor_mode::route_recommend(alignment::vertex &Vertex) const void editor_mode::route_recommend(alignment::vertex &Vertex) const
@@ -1111,3 +1120,157 @@ void editor_mode::render_route_ui()
ImGui::TextUnformatted(route.status.c_str()); ImGui::TextUnformatted(route.status.c_str());
} }
} }
editor_track::straight const &editor_mode::current_straight()
{
auto *track{selected_track()};
if (track != m_straights.current_for)
{
m_straights.current_for = track;
m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{};
}
return m_straights.current;
}
void editor_mode::draw_straights_overlay() const
{
screen_projection const projection;
ImDrawList *drawlist = ImGui::GetBackgroundDrawList();
glm::dvec3 const camera{Global.pCamera.Pos};
auto const nearby = [&](editor_track::straight const &Line) {
auto const offset{camera - Line.start};
auto const along{std::clamp(glm::dot(glm::dvec2{offset.x, offset.z}, Line.direction), 0.0, Line.length)};
auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along};
return glm::distance(glm::dvec2{closest.x, closest.z}, glm::dvec2{camera.x, camera.z}) < 3000.0;
};
auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) {
if (Line.tracks.empty())
return;
projection.line(drawlist, Line.start, Line.end, Color, Thickness);
for (auto const &point : {Line.start, Line.end})
{
ImVec2 screen;
if (projection.project(point, screen))
drawlist->AddCircleFilled(screen, Thickness + 2.0f, Color);
}
};
for (int i = 0; i < static_cast<int>(m_straights.found.size()); ++i)
{
auto const &line{m_straights.found[i]};
if (nearby(line))
draw(line, i == m_straights.listed ? IM_COL32(40, 220, 255, 255) : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f);
}
draw(m_straights.current, IM_COL32(40, 220, 255, 255), 4.0f);
draw(m_straights.a, IM_COL32(60, 230, 90, 255), 5.0f);
draw(m_straights.b, IM_COL32(255, 60, 255, 255), 5.0f);
if (false == m_straights.a.tracks.empty() && false == m_straights.b.tracks.empty())
{
auto const &a{m_straights.a};
for (auto const &point : {m_straights.b.start, m_straights.b.end})
{
auto const along{glm::dot(glm::dvec2{point.x - a.start.x, point.z - a.start.z}, a.direction)};
auto const foot{a.start + glm::dvec3{a.direction.x, 0.0, a.direction.y} * along + glm::dvec3{0.0, a.grade * along, 0.0}};
projection.line(drawlist, foot, point, IM_COL32(255, 210, 60, 220), 1.5f);
}
}
}
void editor_mode::render_straights_ui()
{
auto &state{m_straights};
ImGui::TextDisabled("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to");
ImGui::PushItemWidth(100.0f);
double angle{glm::degrees(state.tolerance.angle)};
if (ImGui::InputDouble("Angle tolerance (deg)", &angle, 0.0, 0.0, "%.4f"))
state.tolerance.angle = glm::radians(std::max(0.0, angle));
ImGui::InputDouble("Offset tolerance (m)", &state.tolerance.offset, 0.0, 0.0, "%.3f");
ImGui::InputDouble("Minimum length (m)", &state.minimum_length, 0.0, 0.0, "%.0f");
ImGui::PopItemWidth();
if (ImGui::Button("Recognize straights in the scenery"))
{
state.found = editor_track::find_straights(state.minimum_length, state.tolerance);
state.listed = -1;
state.current_for = nullptr;
}
auto const describe = [](editor_track::straight const &Line) {
char text[160];
std::snprintf(text, sizeof(text), "L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size());
return std::string{text};
};
if (false == state.found.empty())
{
ImGui::Text("%zu straights", state.found.size());
ImGui::SameLine();
ImGui::PushItemWidth(150.0f);
ImGui::InputTextWithHint("##straightfilter", "path name", state.filter, sizeof(state.filter));
ImGui::PopItemWidth();
ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true);
for (int i = 0; i < static_cast<int>(state.found.size()); ++i)
{
auto const &line{state.found[i]};
if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; }))
continue;
auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)};
if (ImGui::Selectable(label.c_str(), state.listed == i))
{
state.listed = i;
m_node = line.tracks[line.tracks.size() / 2];
ui()->set_node(m_node);
start_focus(m_node);
}
}
ImGui::EndChild();
}
ImGui::Separator();
auto const &line{current_straight()};
if (line.tracks.empty())
{
ImGui::TextDisabled(selected_track() != nullptr ? "The selected path isn't straight" : "No path selected");
}
else
{
ImGui::Text("Straight of the selected path");
ImGui::Indent();
ImGui::TextUnformatted(describe(line).c_str());
ImGui::Text("From (%.3f, %.3f, %.3f)", line.start.x, line.start.y, line.start.z);
ImGui::Text("To (%.3f, %.3f, %.3f)", line.end.x, line.end.y, line.end.z);
ImGui::Text("Paths: %s ... %s", line.tracks.front()->name().c_str(), line.tracks.back()->name().c_str());
ImGui::Unindent();
if (ImGui::Button("Set as A"))
state.a = line;
ImGui::SameLine();
if (ImGui::Button("Set as B"))
state.b = line;
}
ImGui::Separator();
auto const &a{state.a};
auto const &b{state.b};
ImGui::TextColored(ImVec4(0.25f, 0.9f, 0.35f, 1.0f), "A: %s", a.tracks.empty() ? "-" : describe(a).c_str());
ImGui::TextColored(ImVec4(1.0f, 0.25f, 1.0f, 1.0f), "B: %s", b.tracks.empty() ? "-" : describe(b).c_str());
if (a.tracks.empty() || b.tracks.empty())
return;
auto const cross{a.direction.x * b.direction.y - a.direction.y * b.direction.x};
auto const dot{glm::dot(a.direction, b.direction)};
auto const between{glm::degrees(std::atan2(std::abs(cross), std::abs(dot)))};
auto const offset = [&](glm::dvec3 const &Point) {
glm::dvec2 const relative{Point.x - a.start.x, Point.z - a.start.z};
return a.direction.x * relative.y - a.direction.y * relative.x;
};
auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - a.start.x, Point.z - a.start.z}, a.direction); };
auto const height = [&](glm::dvec3 const &Point) { return Point.y - (a.start.y + a.grade * along(Point)); };
auto const startoffset{offset(b.start)};
auto const endoffset{offset(b.end)};
auto const from{std::max(0.0, std::min(along(b.start), along(b.end)))};
auto const to{std::min(a.length, std::max(along(b.start), along(b.end)))};
ImGui::Text("Angle between A and B: %.5f deg%s", between, between < 0.001 ? " (parallel)" : "");
ImGui::Text("Distance of B from A: start %.3f m, end %.3f m, change %.3f m", std::abs(startoffset), std::abs(endoffset), std::abs(endoffset - startoffset));
ImGui::Text("B lies on the %s of A", (startoffset + endoffset) > 0.0 ? "left" : "right");
ImGui::Text("Height of B above A: start %.3f m, end %.3f m", height(b.start), height(b.end));
ImGui::Text("Grade difference: %.2f per mille", (b.grade * (dot < 0.0 ? -1.0 : 1.0) - a.grade) * 1000.0);
ImGui::Text("Overlap along A: %.2f m", std::max(0.0, to - from));
}

View File

@@ -21,7 +21,9 @@ http:
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <deque>
#include <sstream> #include <sstream>
#include <unordered_set>
namespace namespace
{ {
@@ -771,6 +773,119 @@ bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::strin
return true; return true;
} }
bool editor_track::is_straight(TTrack const &Track, straight_tolerance const &Tolerance)
{
if (Track.eType != tt_Normal || false == is_supported(Track) || Track.m_paths.empty())
return false;
auto const &path{Track.m_paths.front()};
glm::dvec2 const start{path.points[segment_data::point::start].x, path.points[segment_data::point::start].z};
glm::dvec2 const end{path.points[segment_data::point::end].x, path.points[segment_data::point::end].z};
auto const length{glm::distance(start, end)};
if (length < 1e-3)
return false;
auto const direction{(end - start) / length};
for (auto const index : {segment_data::point::control1, segment_data::point::control2})
{
auto const &control{path.points[index]};
glm::dvec2 const offset{control.x, control.z};
if (std::abs(direction.x * offset.y - direction.y * offset.x) > Tolerance.offset)
return false;
}
return true;
}
editor_track::straight editor_track::find_straight(TTrack &Track, straight_tolerance const &Tolerance)
{
straight result;
if (false == is_straight(Track, Tolerance))
return result;
auto const plan = [](glm::dvec3 const &Point) { return glm::dvec2{Point.x, Point.z}; };
auto const &first{Track.m_paths.front()};
auto const origin{first.points[segment_data::point::start]};
auto const direction{glm::normalize(plan(first.points[segment_data::point::end] - origin))};
auto const lateral = [&](glm::dvec3 const &Point) {
auto const offset{plan(Point - origin)};
return std::abs(direction.x * offset.y - direction.y * offset.x);
};
auto const continues = [&](TTrack const *Other) {
if (Other == nullptr || false == is_straight(*Other, Tolerance))
return false;
auto const &path{Other->m_paths.front()};
auto const otherdirection{glm::normalize(plan(path.points[segment_data::point::end] - path.points[segment_data::point::start]))};
if (std::abs(direction.x * otherdirection.y - direction.y * otherdirection.x) > Tolerance.angle)
return false;
return lateral(path.points[segment_data::point::start]) <= Tolerance.offset && lateral(path.points[segment_data::point::end]) <= Tolerance.offset;
};
std::deque<TTrack *> run{&Track};
for (auto const throughend : {true, false})
{
TTrack *previous{&Track};
TTrack *current{throughend ? Track.trNext : Track.trPrev};
while (continues(current) && std::find(run.begin(), run.end(), current) == run.end())
{
if (throughend)
run.push_back(current);
else
run.push_front(current);
auto *next{current->trPrev == previous ? current->trNext : current->trPrev};
previous = current;
current = next;
}
}
double low{0.0}, high{0.0};
glm::dvec3 lowpoint{origin}, highpoint{origin};
for (auto *track : run)
{
for (auto const index : {segment_data::point::start, segment_data::point::end})
{
auto const &point{track->m_paths.front().points[index]};
auto const along{glm::dot(plan(point - origin), direction)};
if (along < low)
{
low = along;
lowpoint = point;
}
if (along > high)
{
high = along;
highpoint = point;
}
}
}
result.tracks.assign(run.begin(), run.end());
result.start = lowpoint;
result.end = highpoint;
result.direction = direction;
result.length = high - low;
result.grade = result.length > 0.0 ? (highpoint.y - lowpoint.y) / result.length : 0.0;
auto azimuth{glm::degrees(std::atan2(direction.x, direction.y))};
if (azimuth < 0.0)
azimuth += 360.0;
result.azimuth = azimuth;
return result;
}
std::vector<editor_track::straight> editor_track::find_straights(double const Minimumlength, straight_tolerance const &Tolerance)
{
std::vector<straight> result;
std::unordered_set<TTrack const *> visited;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || visited.count(track) > 0 || false == is_straight(*track, Tolerance))
continue;
auto line{find_straight(*track, Tolerance)};
for (auto const *member : line.tracks)
visited.insert(member);
if (line.length >= Minimumlength)
result.push_back(std::move(line));
}
std::sort(result.begin(), result.end(), [](straight const &A, straight const &B) { return A.length > B.length; });
return result;
}
std::vector<TTrack *> editor_track::relay(chain const &Chain, std::vector<segment_data> const &Pieces) std::vector<TTrack *> editor_track::relay(chain const &Chain, std::vector<segment_data> const &Pieces)
{ {
std::vector<TTrack *> created; std::vector<TTrack *> created;

View File

@@ -95,6 +95,25 @@ class editor_track
double radius{0.0}; double radius{0.0};
}; };
static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error); static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error);
struct straight
{
std::vector<TTrack *> tracks;
glm::dvec3 start{0.0};
glm::dvec3 end{0.0};
glm::dvec2 direction{0.0, 1.0};
double length{0.0};
double grade{0.0};
double azimuth{0.0};
};
struct straight_tolerance
{
double angle{0.0002};
double offset{0.02};
};
static bool is_straight(TTrack const &Track, straight_tolerance const &Tolerance);
static straight find_straight(TTrack &Track, straight_tolerance const &Tolerance);
static std::vector<straight> find_straights(double const Minimumlength, straight_tolerance const &Tolerance);
static std::vector<TTrack *> relay(chain const &Chain, std::vector<segment_data> const &Pieces); static std::vector<TTrack *> relay(chain const &Chain, std::vector<segment_data> const &Pieces);
static void retire(TTrack &Track); static void retire(TTrack &Track);
static void revive(TTrack &Track); static void revive(TTrack &Track);