From b0c363b586167bc101e7032b9ef617127ab2fc1d Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 01:27:41 +0200 Subject: [PATCH] feat: structure gauge along the line of the selected path in the OpenGL view --- CMakeLists.txt | 2 + application/editormode.cpp | 1 + application/editormode.h | 18 ++ application/editormodegauge.cpp | 402 ++++++++++++++++++++++++++++++++ application/editormodetrack.cpp | 1 + editor/editorGauge.cpp | 110 +++++++++ editor/editorGauge.hpp | 55 +++++ editor/editorTrack.cpp | 4 +- editor/editorTrack.hpp | 1 + rendering/editoroverlay.h | 42 ++++ rendering/opengl33renderer.cpp | 57 +++++ rendering/opengl33renderer.h | 4 + 12 files changed, 696 insertions(+), 1 deletion(-) create mode 100644 application/editormodegauge.cpp create mode 100644 editor/editorGauge.cpp create mode 100644 editor/editorGauge.hpp create mode 100644 rendering/editoroverlay.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 94d71aa6..04eb6763 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -173,12 +173,14 @@ set(SOURCES "editor/editorTrack.cpp" "editor/editorAlignment.cpp" "editor/editorProfile.cpp" +"editor/editorGauge.cpp" "editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" "editor/editorOrthophoto.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormodeprofile.cpp" +"application/editormodegauge.cpp" "application/editormodeinfra.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 08f5103c..f074dc05 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -1035,6 +1035,7 @@ bool editor_mode::update() draw_infra_overlay(); render_infra_window(); } + update_gauge(); // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- if (ui()->mode() == nodebank_panel::FILL) diff --git a/application/editormode.h b/application/editormode.h index 62f79ef4..7bdc8c2c 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorAlignment.hpp" #include "editor/editorProfile.hpp" #include "editor/editorInfra.hpp" +#include "editor/editorGauge.hpp" #include #include @@ -305,6 +306,23 @@ class editor_mode : public application_mode, private editor_track::observer std::array, 3> m_track_materials{}; std::array m_track_material_edited{}; // path whose texture name is being typed std::string m_track_mode_notice; + // structure gauge along the line of the selected path, shown in the 3d view + struct gauge_state + { + bool enabled{false}; + std::vector profiles; + int profile{0}; + double reach{500.0}; // m, each way from the path + TTrack const *track{nullptr}; // scanned + std::size_t history{0}; + bool pending{false}; // rescan once the edit ends + std::size_t models{0}; + std::size_t intrusions{0}; // triangles + } m_gauge; + void gauge_load(); + void update_gauge(); + void scan_gauge(TTrack &Track); + void render_gauge_ui(); struct route_design { TTrack *from{nullptr}; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp new file mode 100644 index 00000000..2fbd9192 --- /dev/null +++ b/application/editormodegauge.cpp @@ -0,0 +1,402 @@ +/* +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/editoruilayer.h" +#include "editor/editorGauge.hpp" +#include "editor/editorGeometry.hpp" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/editoroverlay.h" +#include "rendering/renderer.h" +#include "simulation/simulation.h" +#include "world/Track.h" +#include "imgui/imgui.h" +#include +#include +#include +#include +#include + +using geometry::plan_of; + +namespace +{ + +char const *const gauge_file{"editor_gauges.txt"}; +std::size_t const gauge_triangle_limit{200000}; + +// samples of the route, found by the position in the plan +class corridor +{ +public: + struct local + { + std::size_t sample; + double lateral; // positive to the left + double height; // above the rail top + }; + + corridor(std::vector const &Samples, double const Margin) : m_samples{Samples}, m_margin{Margin} + { + m_min = m_max = plan_of(Samples.front().position); + for (auto const &sample : Samples) + { + m_min = glm::min(m_min, plan_of(sample.position)); + m_max = glm::max(m_max, plan_of(sample.position)); + } + m_min -= glm::dvec2(Margin + cell); + m_max += glm::dvec2(Margin + cell); + m_columns = static_cast((m_max.x - m_min.x) / cell) + 1; + m_rows = static_cast((m_max.y - m_min.y) / cell) + 1; + m_cells.resize(static_cast(m_columns) * m_rows); + for (std::size_t i = 0; i < Samples.size(); ++i) + m_cells[index(cell_of(plan_of(Samples[i].position)))].push_back(i); + } + + static glm::dvec2 normal(editor_track::route_sample const &Sample) + { + return {-Sample.direction.y, Sample.direction.x}; + } + + // whether the sphere may reach the gauge + bool near(glm::dvec3 const &Center, double const Radius) const + { + auto const center{plan_of(Center)}; + if (center.x + Radius < m_min.x || center.x - Radius > m_max.x || center.y + Radius < m_min.y || center.y - Radius > m_max.y) + return false; + auto const reach{(Radius + m_margin) * (Radius + m_margin)}; + return std::any_of(m_samples.begin(), m_samples.end(), [&](editor_track::route_sample const &Sample) { return glm::length2(plan_of(Sample.position) - center) <= reach; }); + } + + std::optional locate(glm::dvec3 const &Point) const + { + auto const point{plan_of(Point)}; + auto const cell{cell_of(point)}; + std::size_t nearest{m_samples.size()}; + double best{m_margin * m_margin}; + for (int row = std::max(0, cell.y - 1); row <= std::min(m_rows - 1, cell.y + 1); ++row) + for (int column = std::max(0, cell.x - 1); column <= std::min(m_columns - 1, cell.x + 1); ++column) + for (auto const i : m_cells[index({column, row})]) + { + auto const distance{glm::length2(plan_of(m_samples[i].position) - point)}; + if (distance < best) + { + best = distance; + nearest = i; + } + } + if (nearest == m_samples.size()) + return std::nullopt; + auto const &sample{m_samples[nearest]}; + auto const offset{point - plan_of(sample.position)}; + auto const along{glm::dot(offset, sample.direction)}; + if ((nearest == 0 && along < -0.6) || (nearest + 1 == m_samples.size() && along > 0.6)) + return std::nullopt; + return local{nearest, glm::dot(offset, normal(sample)), Point.y - (sample.position.y + sample.grade * along)}; + } + +private: + static constexpr double cell{8.0}; + + glm::ivec2 cell_of(glm::dvec2 const &Point) const + { + return {std::clamp(static_cast((Point.x - m_min.x) / cell), 0, m_columns - 1), std::clamp(static_cast((Point.y - m_min.y) / cell), 0, m_rows - 1)}; + } + std::size_t index(glm::ivec2 const &Cell) const + { + return static_cast(Cell.y) * m_columns + Cell.x; + } + + std::vector const &m_samples; + double m_margin; + glm::dvec2 m_min, m_max; + int m_columns{1}, m_rows{1}; + std::vector> m_cells; +}; + +gauge::section section_of(editor_track::route_sample const &Sample) +{ + gauge::section section; + if (std::abs(Sample.curvature) > 1e-5) + { + section.radius = 1.0 / std::abs(Sample.curvature); + section.inner = Sample.curvature > 0.0 ? 1 : -1; + } + section.cant = Sample.cant; + return section; +} + +glm::dmat4 placement(TAnimModel const &Instance) +{ + auto const angles{Instance.Angles()}; + auto matrix{glm::translate(glm::dmat4(1.0), Instance.location())}; + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.y)), glm::dvec3(0.0, 1.0, 0.0)); + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.x)), glm::dvec3(1.0, 0.0, 0.0)); + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.z)), glm::dvec3(0.0, 0.0, 1.0)); + return glm::scale(matrix, glm::dvec3(Instance.Scale())); +} + +// calls the visitor with the world corners of the visible triangles of the submodel, its siblings and children +template void visit_triangles(TSubModel *Submodel, glm::dmat4 const &Matrix, Visitor const &Visit) +{ + for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->NextGet()) + { + if (submodel->iVisible == 0) + continue; + auto matrix{Matrix}; + if ((submodel->iFlags & 0xC000) && submodel->GetMatrix() != nullptr) + matrix *= glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray())); + if (submodel->eType == GL_TRIANGLES && submodel->m_geometry.handle != null_handle) + { + auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; + auto const corner{[&](std::size_t const Index) { return glm::dvec3(matrix * glm::dvec4(glm::dvec3(vertices[Index].position), 1.0)); }}; + auto const count{indices.empty() ? vertices.size() : indices.size()}; + for (std::size_t i = 0; i + 2 < count; i += 3) + { + if (indices.empty()) + Visit(corner(i), corner(i + 1), corner(i + 2)); + else + Visit(corner(indices[i]), corner(indices[i + 1]), corner(indices[i + 2])); + } + } + visit_triangles(submodel->ChildGet(), matrix, Visit); + } +} + +} // namespace + +void editor_mode::gauge_load() +{ + if (m_gauge.profiles.empty()) + m_gauge.profiles = gauge::load_profiles(gauge_file); + m_gauge.profile = std::clamp(m_gauge.profile, 0, static_cast(m_gauge.profiles.size()) - 1); +} + +void editor_mode::update_gauge() +{ + auto *track{m_gauge.enabled && ui()->mode() == nodebank_panel::TRACK ? selected_track() : nullptr}; + if (track == nullptr) + { + if (false == EditorOverlay.batches.empty()) + EditorOverlay.clear(); + m_gauge.track = nullptr; + return; + } + // the geometry is rescanned once the edit ends + if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging) + { + m_gauge.pending = true; + return; + } + if (track == m_gauge.track && m_history.size() == m_gauge.history && false == m_gauge.pending) + return; + m_gauge.track = track; + m_gauge.history = m_history.size(); + m_gauge.pending = false; + scan_gauge(*track); +} + +void editor_mode::scan_gauge(TTrack &Track) +{ + gauge_load(); + EditorOverlay.clear(); + m_gauge.intrusions = 0; + m_gauge.models = 0; + auto route{editor_track::run_route(Track, m_gauge.reach)}; + auto const samples{editor_track::sample_route(route, 1.0)}; + auto const &profile{m_gauge.profiles[m_gauge.profile]}; + if (samples.size() < 2 || profile.outline.size() < 2) + return; + + double widest{0.0}, top{0.0}; + for (auto const &point : profile.outline) + { + widest = std::max(widest, point.half_width); + top = std::max(top, point.height); + } + corridor const space{samples, widest + gauge::curve_widening(150.0) + gauge::cant_widening(0.2, top) + 0.5}; + auto const origin{samples.front().position}; + EditorOverlay.origin = origin; + auto const relative{[&](glm::dvec3 const &Point) { return glm::vec3(Point - origin); }}; + auto const lift{[](editor_track::route_sample const &Sample, double const Lateral, double const Height) { + auto const normal{corridor::normal(Sample)}; + return Sample.position + glm::dvec3(normal.x * Lateral, Height, normal.y * Lateral); + }}; + + // outline across the track, from the bottom right up and down to the bottom left + auto const ring{[&](editor_track::route_sample const &Sample) { + auto const section{section_of(Sample)}; + std::vector points; + for (int side : {-1, 1}) + for (std::size_t j = 0; j < profile.outline.size(); ++j) + { + auto const &point{profile.outline[side < 0 ? j : profile.outline.size() - 1 - j]}; + auto width{point.half_width + gauge::curve_widening(section.radius)}; + if (side == section.inner) + width += gauge::cant_widening(section.cant, point.height); + points.push_back(relative(lift(Sample, side * width, point.height))); + } + return points; + }}; + + gfx::editor_overlay::batch surface{{0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES}; + gfx::editor_overlay::batch lines{{0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES}; + gfx::editor_overlay::batch intruding{{1.0f, 0.1f, 0.05f, 0.6f}, GL_TRIANGLES, true}; + std::vector previous; + double lastring{-1e9}; + for (std::size_t i = 0; i < samples.size(); i += 2) + { + auto const current{ring(samples[i])}; + if (false == previous.empty()) + for (std::size_t j = 1; j < current.size(); ++j) + { + surface.points.insert(surface.points.end(), {previous[j - 1], previous[j], current[j], previous[j - 1], current[j], current[j - 1]}); + lines.points.insert(lines.points.end(), {previous[j], current[j]}); + } + if (samples[i].chainage - lastring >= 10.0 || i + 2 >= samples.size()) + { + lastring = samples[i].chainage; + for (std::size_t j = 1; j < current.size(); ++j) + lines.points.insert(lines.points.end(), {current[j - 1], current[j]}); + } + previous = current; + } + + // triangle enters the gauge when any point of a grid spread over it does + auto const inside{[&](glm::dvec3 const &Point) { + auto const local{space.locate(Point)}; + return local && gauge::intrusion(profile, section_of(samples[local->sample]), local->lateral, local->height) > 0.0; + }}; + for (auto *instance : simulation::Instances.sequence()) + { + if (intruding.points.size() >= 3 * gauge_triangle_limit) + break; + if (instance == nullptr || false == instance->visible() || instance->Model() == nullptr || instance->Model()->GetSMRoot() == nullptr) + continue; + if (false == space.near(instance->location(), instance->radius())) + continue; + auto const before{intruding.points.size()}; + visit_triangles(instance->Model()->GetSMRoot(), placement(*instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { + auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})}; + auto const steps{std::clamp(static_cast(std::ceil(edge / 0.5)), 1, 24)}; + for (int u = 0; u <= steps; ++u) + for (int v = 0; u + v <= steps; ++v) + { + auto const a{static_cast(u) / steps}; + auto const b{static_cast(v) / steps}; + if (inside(A + (B - A) * a + (C - A) * b)) + { + intruding.points.insert(intruding.points.end(), {relative(A), relative(B), relative(C)}); + return; + } + } + }); + if (intruding.points.size() > before) + ++m_gauge.models; + } + m_gauge.intrusions = intruding.points.size() / 3; + EditorOverlay.batches = {std::move(surface), std::move(lines), std::move(intruding)}; +} + +void editor_mode::render_gauge_ui() +{ + if (false == ImGui::CollapsingHeader("Structure gauge")) + return; + if (ImGui::Checkbox("Show in the 3D view", &m_gauge.enabled)) + m_gauge.pending = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Translucent tunnel of the gauge along the line of the selected path,\nwith the triangles of the models which enter it in red. Widened in curves and on the inner side of a cant."); + gauge_load(); + auto const previous{m_gauge.profile}; + for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) + { + if (i > 0) + ImGui::SameLine(); + ImGui::RadioButton(m_gauge.profiles[i].name.c_str(), &m_gauge.profile, i); + } + bool changed{previous != m_gauge.profile}; + float reach{static_cast(m_gauge.reach)}; + if (ImGui::SliderFloat("Reach each way (m)", &reach, 50.0f, 2000.0f, "%.0f")) + m_gauge.reach = reach; + changed |= ImGui::IsItemDeactivatedAfterEdit(); + if (m_gauge.enabled) + { + if (m_gauge.models > 0) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%zu models enter the gauge (%zu triangles)", m_gauge.models, m_gauge.intrusions); + else + ImGui::TextDisabled("The gauge is clear"); + } + + if (ImGui::TreeNode("Outline (right half)")) + { + ImGui::TextDisabled("Approximate starting points, check against PN-69/K-02057.\nMetres from the track axis and above the rail top, bottom up."); + auto &profile{m_gauge.profiles[m_gauge.profile]}; + bool edited{false}; + bool inserted{false}; + int remove{-1}; + ImGui::Columns(3, "gaugeoutline", false); + ImGui::SetColumnWidth(0, 110.0f); + ImGui::SetColumnWidth(1, 110.0f); + ImGui::TextDisabled("Half-width"); + ImGui::NextColumn(); + ImGui::TextDisabled("Height"); + ImGui::NextColumn(); + ImGui::NextColumn(); + for (int i = 0; i < static_cast(profile.outline.size()); ++i) + { + ImGui::PushID(i); + auto &point{profile.outline[i]}; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputDouble("##width", &point.half_width, 0.0, 0.0, "%.3f"); + edited |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputDouble("##height", &point.height, 0.0, 0.0, "%.3f"); + edited |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::NextColumn(); + if (ImGui::SmallButton("+")) + { + auto const &next{profile.outline[std::min(i + 1, profile.outline.size() - 1)]}; + profile.outline.insert(profile.outline.begin() + i + 1, {0.5 * (point.half_width + next.half_width), 0.5 * (point.height + next.height)}); + inserted = edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Insert a point after this one"); + ImGui::SameLine(); + if (profile.outline.size() > 2 && ImGui::SmallButton("x")) + remove = i; + ImGui::NextColumn(); + ImGui::PopID(); + if (inserted) + break; + } + ImGui::Columns(1); + if (remove >= 0) + { + profile.outline.erase(profile.outline.begin() + remove); + edited = true; + } + if (ImGui::SmallButton("Restore the starting outlines")) + { + m_gauge.profiles = gauge::default_profiles(); + edited = true; + } + if (edited) + { + gauge::save_profiles(gauge_file, m_gauge.profiles); + changed = true; + } + ImGui::TreePop(); + } + if (changed) + m_gauge.pending = true; +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 4e3b9a94..2fb4ace3 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -482,6 +482,7 @@ void editor_mode::render_track_window() m_infra.scope = 1; infra_recognize(); } + render_gauge_ui(); ImGui::End(); } diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp new file mode 100644 index 00000000..937feb5b --- /dev/null +++ b/editor/editorGauge.cpp @@ -0,0 +1,110 @@ +/* +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 "editor/editorGauge.hpp" + +#include +#include + +namespace gauge +{ + +std::vector default_profiles() +{ + std::vector const lower{{1.645, 0.05}, {1.645, 0.30}, {1.720, 0.30}, {1.720, 1.10}, {2.200, 1.10}, {2.200, 3.85}, {1.900, 4.25}}; + profile plain{"A (bez sieci)", lower}; + plain.outline.insert(plain.outline.end(), {{1.400, 4.85}, {0.0, 4.85}}); + profile electrified{"C (sieć trakcyjna)", lower}; + electrified.outline.insert(electrified.outline.end(), {{1.700, 4.85}, {1.400, 5.60}, {1.000, 6.00}, {0.0, 6.00}}); + return {plain, electrified}; +} + +// one outline per line: the name in quotes, then pairs of half-width and height +std::vector load_profiles(std::string const &File) +{ + std::ifstream input{File}; + std::vector profiles; + std::string line; + while (std::getline(input, line)) + { + std::istringstream stream{line}; + profile profile; + if (false == static_cast(stream >> std::quoted(profile.name))) + continue; + profile::point point; + while (stream >> point.half_width >> point.height) + profile.outline.push_back(point); + if (profile.outline.size() >= 2) + profiles.push_back(std::move(profile)); + } + return profiles.empty() ? default_profiles() : profiles; +} + +void save_profiles(std::string const &File, std::vector const &Profiles) +{ + std::ofstream output{File}; + for (auto const &profile : Profiles) + { + output << std::quoted(profile.name); + for (auto const &point : profile.outline) + output << ' ' << point.half_width << ' ' << point.height; + output << '\n'; + } +} + +double curve_widening(double const Radius) +{ + // smallest radius of the class and its widening in mm; the radius between the classes takes the larger one + static constexpr std::pair table[]{{3500, 10}, {2500, 15}, {1800, 20}, {1500, 25}, {1200, 30}, {1000, 35}, + {900, 40}, {800, 45}, {700, 50}, {600, 60}, {500, 75}, {450, 80}, + {400, 90}, {350, 105}, {300, 120}, {280, 130}, {260, 140}, {250, 145}, + {240, 150}, {220, 165}, {200, 180}, {190, 190}, {180, 200}}; + if (Radius <= 0.0 || Radius >= 4000.0) + return 0.0; + for (auto const &[radius, widening] : table) + if (Radius >= radius) + return widening * 0.001; + return 36.0 / Radius; +} + +double cant_widening(double const Cant, double const Height) +{ + return Height * Cant / 1.5; +} + +double half_width(profile const &Profile, section const &Section, double const Height, int const Side) +{ + double width{-1.0}; + for (std::size_t i = 1; i < Profile.outline.size(); ++i) + { + auto const &a{Profile.outline[i - 1]}; + auto const &b{Profile.outline[i]}; + if (Height < std::min(a.height, b.height) || Height > std::max(a.height, b.height)) + continue; + auto const span{b.height - a.height}; + width = std::max(width, std::abs(span) < 1e-9 ? std::max(a.half_width, b.half_width) : a.half_width + (b.half_width - a.half_width) * (Height - a.height) / span); + } + if (width < 0.0) + return width; + width += curve_widening(Section.radius); + if (Section.inner != 0 && Side == Section.inner) + width += cant_widening(Section.cant, Height); + return width; +} + +double intrusion(profile const &Profile, section const &Section, double const Lateral, double const Height) +{ + auto const width{half_width(Profile, Section, Height, Lateral >= 0.0 ? 1 : -1)}; + if (width < 0.0) + return 0.0; + return width - std::abs(Lateral); +} + +} // namespace gauge diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp new file mode 100644 index 00000000..e4d235bc --- /dev/null +++ b/editor/editorGauge.hpp @@ -0,0 +1,55 @@ +/* +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/. +*/ + +#pragma once + +#include +#include + +// structure gauge (skrajnia budowli): the space along the track which has to stay clear of the objects +namespace gauge +{ + +// right half of the outline on a straight track, in metres: half-width from the axis and height above the rail top, +// listed from the bottom up; the left half is the mirror +struct profile +{ + std::string name; + struct point + { + double half_width{0.0}; + double height{0.0}; + }; + std::vector outline; +}; + +// starting points of the outlines, approximations to verify against PN-69/K-02057 +std::vector default_profiles(); +// outlines kept in the file, the defaults when there's none +std::vector load_profiles(std::string const &File); +void save_profiles(std::string const &File, std::vector const &Profiles); + +// widening of both sides in a curve, from the radius (table 1 of the D1 annex 11), metres +double curve_widening(double Radius); +// widening of the inner side of a curve from the tilt of the vehicles on the cant, at the height above the rail top, metres +double cant_widening(double Cant, double Height); + +// place of the gauge across the track +struct section +{ + double radius{0.0}; // 0: straight + int inner{0}; // side of the curve centre, +1 left or -1 right, 0 on a straight + double cant{0.0}; // metres +}; +// half-width of the gauge at the height on the side (+1 left, -1 right), negative above or below the outline +double half_width(profile const &Profile, section const &Section, double Height, int Side); +// depth of the point inside the gauge, metres, not positive outside of it. Lateral positive to the left +double intrusion(profile const &Profile, section const &Section, double Lateral, double Height); + +} // namespace gauge diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp index e328cbb3..d43d3043 100644 --- a/editor/editorTrack.cpp +++ b/editor/editorTrack.cpp @@ -1406,7 +1406,8 @@ std::vector editor_track::sample_route(route &Route, for (std::size_t i = 0; i < Route.spans.size(); ++i) { auto &span{Route.spans[i]}; - bezier const curve{span.track->m_paths[span.path]}; + auto const &path{span.track->m_paths[span.path]}; + bezier const curve{path}; auto const count{std::max(2, static_cast(std::ceil(curve.plan_length(16) / std::max(0.1, Step))))}; span.from = chainage; auto previous{plan_of(curve.point(span.forward ? 0.0 : 1.0))}; @@ -1425,6 +1426,7 @@ std::vector editor_track::sample_route(route &Route, sample.chainage = chainage; sample.position = position; sample.span = i; + sample.cant = 1.5 * std::sin(std::abs(glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t))); if (planar > 1e-9) { sample.direction = plan_of(derivative) / planar; diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp index 742e393d..d45ecf54 100644 --- a/editor/editorTrack.hpp +++ b/editor/editorTrack.hpp @@ -175,6 +175,7 @@ class editor_track glm::dvec2 direction{0.0, 1.0}; double grade{0.0}; double curvature{0.0}; // signed, 1/m, positive turning left + double cant{0.0}; // m, raise of the outer rail std::size_t span{0}; }; static std::vector sample_route(route &Route, double const Step); diff --git a/rendering/editoroverlay.h b/rendering/editoroverlay.h new file mode 100644 index 00000000..6a533165 --- /dev/null +++ b/rendering/editoroverlay.h @@ -0,0 +1,42 @@ +/* +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/. +*/ + +#pragma once + +#include + +#include + +namespace gfx +{ + +// flat coloured geometry the editor shows in the 3d view, drawn with the depth test over the translucent scenery +struct editor_overlay +{ + struct batch + { + glm::vec4 color{1.0f}; + unsigned int type{0}; // GL primitive + bool offset{false}; // pulled towards the camera, to show over the coplanar surfaces + std::vector points; // relative to the origin + }; + glm::dvec3 origin{0.0}; + std::vector batches; + unsigned int revision{0}; // changed with every new content + + void clear() + { + batches.clear(); + ++revision; + } +}; + +} // namespace gfx + +inline gfx::editor_overlay EditorOverlay; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 682ca59e..4c769e8f 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "rendering/opengl33geometrybank.h" #include "rendering/screenshot.h" +#include "rendering/editoroverlay.h" #include //#define EU07_DEBUG_OPENGL @@ -933,6 +934,7 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode) glDebug("render translucent region"); setup_drawing(true); Render_Alpha(simulation::Region); + Render_editor_overlay(); // particles Render_particles(); @@ -4489,6 +4491,61 @@ void opengl33_renderer::Render_Alpha(TTraction *Traction) glLineWidth(1.0f); } +void opengl33_renderer::Render_editor_overlay() +{ + if (false == EditorModeFlag || EditorOverlay.batches.empty()) + return; + glDebug("Render_editor_overlay"); + + if (m_editor_overlay_revision != EditorOverlay.revision) + { + m_editor_overlay_revision = EditorOverlay.revision; + if (m_editor_overlay_bank == null_handle) + m_editor_overlay_bank = Create_Bank(); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size(); ++i) + { + gfx::vertex_array vertices; + vertices.reserve(batches[i].points.size()); + for (auto const &point : batches[i].points) + vertices.emplace_back(point, glm::vec3(0.f, 1.f, 0.f), glm::vec2()); + gfx::userdata_array userdata; + if (i >= m_editor_overlay_geometry.size()) + m_editor_overlay_geometry.emplace_back(); + auto &geometry{m_editor_overlay_geometry[i]}; + if (geometry != null_handle) + m_geometry.replace(vertices, userdata, geometry); + else if (false == vertices.empty()) + geometry = m_geometry.create_chunk(vertices, userdata, m_editor_overlay_bank, batches[i].type); + } + } + + ::glPushMatrix(); + auto const origin{EditorOverlay.origin - m_renderpass.pass_camera.position()}; + ::glTranslated(origin.x, origin.y, origin.z); + ::glDisable(GL_CULL_FACE); + ::glDepthMask(GL_FALSE); + m_line_shader->bind(); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size() && i < m_editor_overlay_geometry.size(); ++i) + { + if (batches[i].points.empty() || m_editor_overlay_geometry[i] == null_handle) + continue; + if (batches[i].offset) + { + ::glEnable(GL_POLYGON_OFFSET_FILL); + ::glPolygonOffset(-2.f, -2.f); + } + model_ubs.param[0] = batches[i].color; + draw(m_editor_overlay_geometry[i]); + ::glDisable(GL_POLYGON_OFFSET_FILL); + ++m_renderpass.draw_stats.drawcalls; + } + ::glDepthMask(GL_TRUE); + ::glEnable(GL_CULL_FACE); + ::glPopMatrix(); +} + void opengl33_renderer::Render_Alpha(scene::lines_node const &Lines) { glDebug("Render_Alpha scene::lines_node"); diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 9d9bba8c..506e9211 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -306,6 +306,7 @@ class opengl33_renderer : public gfx_renderer { void Render_Alpha(TAnimModel *Instance); void Render_Alpha(TTraction *Traction); void Render_Alpha(scene::lines_node const &Lines); + void Render_editor_overlay(); bool Render_Alpha(TDynamicObject *Dynamic); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance); @@ -449,6 +450,9 @@ class opengl33_renderer : public gfx_renderer { std::unordered_map> m_shaders; std::unique_ptr m_line_shader; + gfx::geometrybank_handle m_editor_overlay_bank; + std::vector m_editor_overlay_geometry; + unsigned int m_editor_overlay_revision{0}; std::unique_ptr m_freespot_shader; std::unique_ptr m_billboard_shader; std::unique_ptr m_celestial_shader;