17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 07:29:20 +02:00

feat(editor): switch templates, auto distances with snapping

This commit is contained in:
maj00r
2026-10-02 23:56:19 +02:00
parent 5caefa297b
commit 32798a32bb
4 changed files with 326 additions and 72 deletions

View File

@@ -332,8 +332,8 @@ class editor_mode : public application_mode
int listed{-1};
TTrack *current_for{nullptr};
editor_track::straight current;
editor_track::straight a;
editor_track::straight b;
std::vector<editor_track::straight> neighbours;
std::vector<double> spacings{3.5, 4.0, 4.5, 4.75, 5.0, 5.5, 6.0};
char filter[32]{};
int handle{-1};
bool dragging{false};
@@ -378,6 +378,9 @@ class editor_mode : public application_mode
double straight_tool_radius(editor_track::straight const &Line) const;
void straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function<glm::dvec3(glm::dvec3 const &)> const &Tail, double const Radius);
void straight_refresh();
void find_neighbour_straights();
glm::dvec3 snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const;
glm::dvec3 snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const;
straights_state m_straights;
enum class track_tab { straights, route, path };
track_tab m_track_tab{track_tab::straights};

View File

@@ -15,6 +15,8 @@ http:
#include "utilities/Globals.h"
#include "rendering/renderer.h"
#include "world/Track.h"
#include "scene/scene.h"
#include "simulation/simulation.h"
#include "utilities/Logs.h"
#include "imgui/imgui.h"
@@ -1232,6 +1234,7 @@ editor_track::straight const &editor_mode::current_straight()
m_straights.current_for = track;
m_straights.handle = -1;
m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{};
find_neighbour_straights();
}
return m_straights.current;
}
@@ -1326,20 +1329,119 @@ void editor_mode::draw_straights_overlay() const
drawlist->AddCircle(screen, 12.0f, IM_COL32(255, 255, 255, 255), 16, 2.5f);
}
}
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 &line{m_straights.current};
if (line.tracks.empty() || m_straights.dragging)
return;
glm::dvec2 const normal{-line.direction.y, line.direction.x};
for (auto const &other : m_straights.neighbours)
{
auto const &a{m_straights.a};
for (auto const &point : {m_straights.b.start, m_straights.b.end})
auto const along = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, line.direction); };
auto const across = [&](glm::dvec3 const &Point) { return glm::dot(glm::dvec2{Point.x - line.start.x, Point.z - line.start.z}, normal); };
auto const a0{along(other.start)}, a1{along(other.end)};
auto const from{std::max(0.0, std::min(a0, a1))};
auto const to{std::min(line.length, std::max(a0, a1))};
if (to <= from)
continue;
auto const middle{(from + to) * 0.5};
auto const fraction{std::abs(a1 - a0) > 1e-6 ? (middle - a0) / (a1 - a0) : 0.0};
auto const lateral{across(other.start) + (across(other.end) - across(other.start)) * fraction};
glm::dvec3 const foot{line.start.x + line.direction.x * middle, line.start.y + line.grade * middle, line.start.z + line.direction.y * middle};
glm::dvec3 const target{foot.x + normal.x * lateral, foot.y, foot.z + normal.y * lateral};
projection.line(drawlist, foot, target, IM_COL32(255, 230, 120, 230), 1.5f);
ImVec2 screen;
if (projection.project((foot + target) * 0.5, screen))
{
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);
auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))};
char label[48];
if (angle > 0.001)
std::snprintf(label, sizeof(label), "%.3f m %.3f deg", std::abs(lateral), angle);
else
std::snprintf(label, sizeof(label), "%.3f m", std::abs(lateral));
auto const size{ImGui::CalcTextSize(label)};
drawlist->AddRectFilled(ImVec2(screen.x - 3.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 3.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 170), 3.0f);
drawlist->AddText(screen, IM_COL32(255, 230, 120, 255), label);
}
}
}
void editor_mode::find_neighbour_straights()
{
auto &state{m_straights};
state.neighbours.clear();
auto const &line{state.current};
if (line.tracks.empty())
return;
glm::dvec2 const normal{-line.direction.y, line.direction.x};
auto const middle{(line.start + line.end) * 0.5};
std::vector<TTrack const *> visited(line.tracks.begin(), line.tracks.end());
auto const sections{simulation::Region->sections(middle, static_cast<float>(line.length * 0.5 + 60.0))};
for (auto *section : sections)
{
for (auto const &cell : section->m_cells)
{
for (auto *track : cell.m_paths)
{
if (std::find(visited.begin(), visited.end(), track) != visited.end() || false == editor_track::is_straight(*track, state.tolerance))
continue;
auto other{editor_track::find_straight(*track, state.tolerance)};
visited.insert(visited.end(), other.tracks.begin(), other.tracks.end());
if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(10.0)))
continue;
auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}};
if (std::abs(glm::dot(offset, normal)) > 60.0)
continue;
auto const a0{glm::dot(glm::dvec2{other.start.x - line.start.x, other.start.z - line.start.z}, line.direction)};
auto const a1{glm::dot(glm::dvec2{other.end.x - line.start.x, other.end.z - line.start.z}, line.direction)};
if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length)
continue;
state.neighbours.push_back(std::move(other));
}
}
}
}
glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const
{
glm::dvec2 const normal{-Line.direction.y, Line.direction.x};
auto const middle{(Line.start + Line.end) * 0.5 + Offset};
double best{0.2};
double correction{0.0};
for (auto const &other : m_straights.neighbours)
{
if (std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x) > std::sin(glm::radians(0.5)))
continue;
auto const distance{glm::dot(glm::dvec2{other.start.x - middle.x, other.start.z - middle.z}, normal)};
for (auto const spacing : m_straights.spacings)
{
auto const error{std::abs(distance) - spacing};
if (std::abs(error) < best)
{
best = std::abs(error);
correction = distance > 0.0 ? error : -error;
}
}
}
return Offset + glm::dvec3{normal.x, 0.0, normal.y} * correction;
}
glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const
{
glm::dvec2 const offset{Moved.x - Pivot.x, Moved.z - Pivot.z};
auto const length{glm::length(offset)};
if (length < 1e-3)
return Moved;
auto const direction{offset / length};
for (auto const &other : m_straights.neighbours)
{
auto const sine{direction.x * other.direction.y - direction.y * other.direction.x};
if (std::abs(sine) > std::sin(glm::radians(0.5)))
continue;
auto const aligned{other.direction * (glm::dot(direction, other.direction) < 0.0 ? -1.0 : 1.0)};
return {Pivot.x + aligned.x * length, Moved.y, Pivot.z + aligned.y * length};
}
return Moved;
}
void editor_mode::render_straights_ui()
{
auto &state{m_straights};
@@ -1403,12 +1505,6 @@ void editor_mode::render_straights_ui()
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::SameLine();
if (in_straight_set(line))
{
if (ImGui::Button("Remove from the set"))
@@ -1496,48 +1592,11 @@ void editor_mode::render_straights_ui()
state.set.clear();
}
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));
ImGui::PushItemWidth(120.0f);
ImGui::InputDouble("Distance (m)", &state.distance, 0.0, 0.0, "%.3f");
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button("Make B parallel to A at this distance"))
if (false == state.neighbours.empty())
{
auto const side{(startoffset + endoffset) >= 0.0 ? 1.0 : -1.0};
glm::dvec2 const direction{a.direction * (dot < 0.0 ? -1.0 : 1.0)};
glm::dvec2 const normal{-a.direction.y, a.direction.x};
auto const startalong{along(b.start)};
auto const start2d{glm::dvec2{a.start.x, a.start.z} + a.direction * startalong + normal * (side * std::abs(state.distance))};
auto const end2d{start2d + direction * b.length};
straight_apply(b, {start2d.x, b.start.y, start2d.y}, {end2d.x, b.end.y, end2d.y});
ImGui::Separator();
ImGui::Text("Neighbouring straights: %zu (distances shown in the view)", state.neighbours.size());
ImGui::TextDisabled("Dragging the middle snaps the distance to: 3.5, 4.0, 4.5, 4.75, 5.0, 5.5, 6.0 m; dragging an end snaps parallel");
}
}
@@ -1580,9 +1639,7 @@ void editor_mode::straight_refresh()
auto &state{m_straights};
state.current_for = selected_track();
state.current = state.current_for != nullptr ? editor_track::find_straight(*state.current_for, state.tolerance) : editor_track::straight{};
for (auto *line : {&state.a, &state.b})
if (false == line->tracks.empty())
*line = editor_track::find_straight(*line->tracks.front(), state.tolerance);
find_neighbour_straights();
for (auto &line : state.found)
if (false == line.tracks.empty())
line = editor_track::find_straight(*line.tracks.front(), state.tolerance);
@@ -1696,17 +1753,18 @@ void editor_mode::render_straight_gizmo()
m_track_drag = tracks;
}
auto const &grabbed{state.drag_line};
glm::dvec3 const moved{camerapos + glm::dvec3(state.gizmo[3])};
glm::dvec3 moved{camerapos + glm::dvec3(state.gizmo[3])};
std::function<glm::dvec3(glm::dvec3 const &)> transform;
if (state.handle == 2)
{
auto const offset{moved - (grabbed.start + grabbed.end) * 0.5};
auto const offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)};
transform = [offset](glm::dvec3 const &Point) { return Point + offset; };
}
else
{
auto const pivot{state.handle == 0 ? grabbed.end : grabbed.start};
auto const original{state.handle == 0 ? grabbed.start : grabbed.end};
moved = snap_straight_direction(pivot, moved);
glm::dvec2 const before{original.x - pivot.x, original.z - pivot.z};
glm::dvec2 const after{moved.x - pivot.x, moved.z - pivot.z};
auto const lengthbefore{glm::length(before)};

View File

@@ -22,6 +22,7 @@ http:
#include <cmath>
#include <limits>
#include <deque>
#include <map>
#include <numeric>
#include <functional>
#include <sstream>
@@ -1043,6 +1044,176 @@ bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance
return true;
}
std::vector<editor_track::switch_template> editor_track::find_switch_templates()
{
std::vector<switch_template> result;
std::map<std::string, std::size_t> lookup;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2 || (track->iCategoryFlag & 0x80) != 0)
continue;
auto const &main{track->m_paths[0]};
auto const origin{main.points[segment_data::point::start]};
auto const chord{plan_of(main.points[segment_data::point::end] - origin)};
if (glm::length(chord) < 1.0)
continue;
auto const axis{glm::normalize(chord)};
glm::dvec2 const normal{-axis.y, axis.x};
auto const tolocal = [&](glm::dvec3 const &Vector) {
auto const planar{plan_of(Vector)};
return glm::dvec3{glm::dot(planar, normal), Vector.y, glm::dot(planar, axis)};
};
switch_template entry;
entry.source = track;
for (int i = 0; i < 2; ++i)
{
auto const &path{track->m_paths[i]};
auto &local{entry.local[i]};
local = path;
local.points[segment_data::point::start] = tolocal(path.points[segment_data::point::start] - origin);
local.points[segment_data::point::end] = tolocal(path.points[segment_data::point::end] - origin);
local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]);
local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]);
}
if (glm::length(glm::dvec2{entry.local[1].points[segment_data::point::start].x, entry.local[1].points[segment_data::point::start].z}) > kSamePoint)
continue;
if (entry.local[1].points[segment_data::point::end].x < 0.0)
{
for (auto &path : entry.local)
{
for (auto &point : path.points)
point.x = -point.x;
path.rolls = {-path.rolls[0], -path.rolls[1]};
}
}
entry.length = entry.local[0].points[segment_data::point::end].z;
auto const tangent{path_tangent(entry.local[1], true)};
auto const angle{std::atan2(std::abs(tangent.x), tangent.z)};
entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0;
entry.radius = path_radius(entry.local[1], 0.25);
char key[256];
std::snprintf(key, sizeof(key), "%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str());
auto const found{lookup.find(key)};
if (found != lookup.end())
{
++result[found->second].count;
continue;
}
entry.count = 1;
char label[256];
std::snprintf(label, sizeof(label), "L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str());
entry.label = label;
lookup.emplace(key, result.size());
result.push_back(entry);
}
std::sort(result.begin(), result.end(), [](switch_template const &A, switch_template const &B) { return A.count > B.count; });
return result;
}
std::vector<segment_data> editor_track::place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade)
{
std::vector<segment_data> result;
glm::dvec2 const normal{-Direction.y, Direction.x};
auto const side{static_cast<double>(Side)};
auto const vector = [&](glm::dvec3 const &Local) {
auto const planar{Direction * Local.z + normal * (Local.x * side)};
return glm::dvec3{planar.x, Local.y + Grade * Local.z, planar.y};
};
for (auto const &local : Template.local)
{
segment_data path{local};
path.points[segment_data::point::start] = Origin + vector(local.points[segment_data::point::start]);
path.points[segment_data::point::end] = Origin + vector(local.points[segment_data::point::end]);
path.points[segment_data::point::control1] = vector(local.points[segment_data::point::control1]);
path.points[segment_data::point::control2] = vector(local.points[segment_data::point::control2]);
if (Side < 0)
path.rolls = {-local.rolls[0], -local.rolls[1]};
result.push_back(path);
}
return result;
}
std::vector<editor_track::switch_template> editor_track::standard_switch_templates()
{
struct definition
{
char const *name;
double radius;
double ratio;
double a;
double b;
};
definition const definitions[] = {
{"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615},
{"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615},
{"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0},
{"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0},
{"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943},
{"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0},
};
std::vector<switch_template> result;
for (auto const &definition : definitions)
{
auto const angle{std::atan(1.0 / definition.ratio)};
auto const a{definition.a > 0.0 ? definition.a : definition.radius * std::tan(angle * 0.5)};
auto const estimated{definition.b <= 0.0};
auto const b{estimated ? 1.87 * definition.ratio : definition.b};
auto const offset{b * std::tan(angle)};
switch_template entry;
entry.a = a;
entry.b = b;
entry.length = a + b;
entry.ratio = definition.ratio;
entry.radius = definition.radius;
auto &main{entry.local[0]};
main.points[segment_data::point::end] = {0.0, 0.0, a + b};
auto &diverging{entry.local[1]};
diverging.points[segment_data::point::end] = {offset, 0.0, a + b};
diverging.points[segment_data::point::control1] = {0.0, 0.0, a * 2.0 / 3.0};
diverging.points[segment_data::point::control2] = {-offset * 2.0 / 3.0, 0.0, -b * 2.0 / 3.0};
diverging.radius = static_cast<float>(definition.radius);
entry.label = std::string{definition.name} + " a " + std::to_string(a).substr(0, std::to_string(a).find('.') + 4) + " b " + std::to_string(b).substr(0, std::to_string(b).find('.') + 4) + (estimated ? " (b est.)" : "");
result.push_back(entry);
}
return result;
}
TTrack *editor_track::create_switch(switch_template const &Template, std::vector<segment_data> const &Paths, TTrack const &Style)
{
if (Paths.size() < 2)
return nullptr;
TTrack *track{nullptr};
if (Template.source != nullptr)
{
track = clone(*Template.source);
}
else
{
char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"};
auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"};
std::ostringstream text;
text.precision(std::numeric_limits<double>::digits10);
text << "switch " << Template.length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << 10.0 << ' ' << Style.iQualityFlag << ' ' << 0 << ' ' << environment << ' ';
if (Style.m_visible)
text << "vis " << material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << material_name(Style.m_material2) << ' ' << texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' ';
else
text << "unvis ";
for (int i = 0; i < 2; ++i)
{
auto const &path{Paths[i]};
auto const &start{path.points[segment_data::point::start]};
auto const &control1{path.points[segment_data::point::control1]};
auto const &control2{path.points[segment_data::point::control2]};
auto const &end{path.points[segment_data::point::end]};
text << start.x << ' ' << start.y << ' ' << start.z << ' ' << path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << path.rolls[1] << ' ' << path.radius << ' ';
}
text << "endtrack\n";
track = load_path(text.str(), Style);
}
track->m_paths.assign(Paths.begin(), Paths.begin() + 2);
return track;
}
std::vector<TTrack *> editor_track::straight_affected(std::vector<straight> const &Lines)
{
std::vector<TTrack *> result;
@@ -1252,18 +1423,23 @@ TTrack *editor_track::clone(TTrack const &Template)
{
std::ostringstream text;
Template.export_as_text_(text);
cParser parser(text.str(), cParser::buffer_TEXT);
parser.getTokens();
std::string token;
parser >> token;
auto content{text.str()};
auto const type{content.find("track ")};
if (type == 0)
content.erase(0, 6);
auto *track{load_path(content, Template)};
track->m_friction = Template.m_friction;
return track;
}
TTrack *editor_track::load_path(std::string const &Text, TTrack const &Template)
{
cParser parser(Text, cParser::buffer_TEXT);
scene::node_data data;
data.type = "track";
if (false == Template.name().empty() && Template.name() != "none")
{
for (int i = 1; data.name.empty() || simulation::Paths.find(data.name) != nullptr; ++i)
data.name = Template.name() + "_" + std::to_string(i);
}
auto const base{Template.name().empty() || Template.name() == "none" ? std::string{"editor_track"} : Template.name()};
for (int i = 1; data.name.empty() || simulation::Paths.find(data.name) != nullptr; ++i)
data.name = base + "_" + std::to_string(i);
auto *track = new TTrack(data);
track->m_rangesquaredmin = Template.m_rangesquaredmin;
track->m_rangesquaredmax = Template.m_rangesquaredmax;
@@ -1271,7 +1447,6 @@ TTrack *editor_track::clone(TTrack const &Template)
for (auto *events : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all})
events->clear();
track->m_events = false;
track->m_friction = Template.m_friction;
simulation::Paths.insert(track);
return track;
}

View File

@@ -9,6 +9,7 @@ http:
#pragma once
#include <array>
#include <functional>
#include <string>
#include <utility>
@@ -129,6 +130,22 @@ class editor_track
double cant{0.0};
};
static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve);
struct switch_template
{
TTrack *source{nullptr};
std::array<segment_data, 2> local;
double length{0.0};
double ratio{0.0};
double radius{0.0};
int count{0};
std::string label;
double a{0.0};
double b{0.0};
};
static std::vector<switch_template> standard_switch_templates();
static std::vector<switch_template> find_switch_templates();
static std::vector<segment_data> place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade);
static TTrack *create_switch(switch_template const &Template, std::vector<segment_data> const &Paths, TTrack const &Style);
static void move_straights(std::vector<straight> const &Lines, std::vector<std::pair<glm::dvec3, glm::dvec3>> const &Ends);
static std::vector<TTrack *> relay(chain const &Chain, std::vector<segment_data> const &Pieces);
static void retire(TTrack &Track);
@@ -152,5 +169,6 @@ class editor_track
static void store_switch_path(TTrack &Switch, int const Path);
static void rebuild_geometry(TTrack &Track);
static TTrack *clone(TTrack const &Template);
static TTrack *load_path(std::string const &Text, TTrack const &Template);
static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function<bool(TTrack const *)> const &Member);
};