17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 10:59:20 +02:00
Files
maszyna/application/editormodegauge.cpp

399 lines
14 KiB
C++

/*
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 <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <cmath>
#include <optional>
using geometry::plan_of;
namespace
{
char const *const gauge_file{"editor_structure_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<editor_track::route_sample> 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<int>((m_max.x - m_min.x) / cell) + 1;
m_rows = static_cast<int>((m_max.y - m_min.y) / cell) + 1;
m_cells.resize(static_cast<std::size_t>(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<local> 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<int>((Point.x - m_min.x) / cell), 0, m_columns - 1), std::clamp(static_cast<int>((Point.y - m_min.y) / cell), 0, m_rows - 1)};
}
std::size_t index(glm::ivec2 const &Cell) const
{
return static_cast<std::size_t>(Cell.y) * m_columns + Cell.x;
}
std::vector<editor_track::route_sample> const &m_samples;
double m_margin;
glm::dvec2 m_min, m_max;
int m_columns{1}, m_rows{1};
std::vector<std::vector<std::size_t>> m_cells;
};
std::vector<gauge::section> sections_of(std::vector<editor_track::route_sample> const &Samples)
{
std::vector<double> chainage, curvature, cant;
for (auto const &sample : Samples)
{
chainage.push_back(sample.chainage);
curvature.push_back(sample.curvature);
cant.push_back(sample.cant);
}
return gauge::sections(chainage, curvature, cant);
}
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<double>(angles.y)), glm::dvec3(0.0, 1.0, 0.0));
matrix = glm::rotate(matrix, glm::radians(static_cast<double>(angles.x)), glm::dvec3(1.0, 0.0, 0.0));
matrix = glm::rotate(matrix, glm::radians(static_cast<double>(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 <typename Visitor> 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<int>(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;
auto const sections{sections_of(samples)};
double widest{0.0};
for (auto const &point : profile.outline)
widest = std::max(widest, point.half_width);
// room for the widening in the sharpest curves on the largest cant
corridor const space{samples, widest + 1.2};
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{[&](std::size_t const Index) {
std::vector<glm::vec3> 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 const width{point.half_width + gauge::widening(sections[Index], point.height, side)};
points.push_back(relative(lift(samples[Index], 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<glm::vec3> previous;
double lastring{-1e9};
for (std::size_t i = 0; i < samples.size(); i += 2)
{
auto const current{ring(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, sections[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<int>(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<double>(u) / steps};
auto const b{static_cast<double>(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.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II.");
gauge_load();
auto const previous{m_gauge.profile};
for (int i = 0; i < static_cast<int>(m_gauge.profiles.size()); ++i)
{
if (i % 2 == 1)
ImGui::SameLine(160.0f);
ImGui::RadioButton(m_gauge.profiles[i].name.c_str(), &m_gauge.profile, i);
}
bool changed{previous != m_gauge.profile};
float reach{static_cast<float>(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("PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\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<int>(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<std::size_t>(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;
}