17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 00:47:56 +01:00

feat(editor): the branch of a switch leads into the free end of a track: when the angles match the switch slides so its branch lies on the track's line, otherwise an arc of the switch radius right after the switch turns into it (with a straight between for a parallel track); works while placing a switch (drag toward the end) and when dragging from the branch end of a placed one, the track is shortened when it reaches past the branch; self-test switchlink / branchlink

This commit is contained in:
maj00r
2026-10-11 23:53:18 +02:00
parent f1a3bde00f
commit 0a73550c8d
6 changed files with 522 additions and 2 deletions

View File

@@ -1072,6 +1072,10 @@ class editor_mode : public application_mode, private editor_track::observer
editor_track::snap_target origin;
editor_track::snap_target snap;
glm::vec2 pressed{0.f};
bool slides{false};
double slide_low{0.0};
double slide_high{0.0};
std::vector<TTrack *> line;
};
extend_tool m_extend;
// lays new track through the clicked points, with no path needed to start from
@@ -1124,6 +1128,22 @@ class editor_mode : public application_mode, private editor_track::observer
std::vector<segment_data> paths;
};
bool plan_crossover(crossover_plan &Plan) const;
struct switch_link
{
editor_track::snap_target target;
editor_track::branch_link link;
double along{0.0};
int forward{1};
int side{1};
std::vector<segment_data> paths;
std::string reason;
};
bool plan_switch_link(switch_link &Plan) const;
void insert_linked_switch(switch_link const &Plan);
bool plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const;
void finish_branch_link(editor_track::branch_link const &Link);
std::string link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const;
std::vector<TTrack *> link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created);
bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created, std::vector<TTrack *> &Removed, TTrack **Style);
bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape);
bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape);

View File

@@ -1460,6 +1460,104 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com
WriteLog("SELFTEST " + command + ": " + tool.status + " " + m_straights.status);
return selftest_result::handled;
}
if (command == "switchlink" || command == "branchlink")
{
std::string label;
glm::dvec3 point{0.0};
glm::dvec3 target{0.0};
if (command == "switchlink")
words >> label;
words >> point.x >> point.z >> target.x >> target.z;
std::replace(label.begin(), label.end(), '_', ' ');
auto const log_link = [&](editor_track::branch_link const &Link, std::string const &Reason) {
WriteLog(format("SELFTEST %s plan: valid %d, shift %.3f m, R %.1f m, before %.3f m, after %.3f m, trim %.3f m, shortfall %.3f m, %zu pieces%s", command.c_str(), Link.valid ? 1 : 0, Link.shift, Link.radius, Link.before, Link.after, Link.trim,
Link.shortfall, Link.pieces.size(), Reason.empty() ? "" : (", " + Reason).c_str()));
for (auto const &piece : Link.pieces)
{
auto const &a{piece.points[segment_data::point::start]};
auto const &b{piece.points[segment_data::point::end]};
WriteLog(format("SELFTEST %s piece: %.3f %.3f %.3f -> %.3f %.3f %.3f, L %.3f m, R %.1f m", command.c_str(), a.x, a.y, a.z, b.x, b.y, b.z, geometry::bezier{piece}.plan_length(), static_cast<double>(piece.radius)));
}
};
if (command == "switchlink")
{
show_track_tab(track_tab::turnout);
auto &tool{m_switch};
tool.templates = editor_track::standard_switch_templates();
tool.collected = true;
tool.armed = -1;
for (int i = 0; i < static_cast<int>(tool.templates.size()); ++i)
if (tool.templates[i].label.rfind(label, 0) == 0)
{
tool.armed = i;
break;
}
TTrack *nearest{nullptr};
double distance{1e9};
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || track->m_paths.empty())
continue;
auto const d{geometry::plan_distance(editor_track::point_at(*track, editor_track::nearest_parameter(*track, point)), point)};
if (d < distance)
{
distance = d;
nearest = track;
}
}
if (tool.armed < 0 || nearest == nullptr)
{
WriteLog("SELFTEST switchlink: no template " + label + " or no path");
return selftest_result::handled;
}
m_node = nearest;
ui()->set_node(m_node);
m_cursor_override = point;
if (false == start_switch_placement())
{
m_cursor_override.reset();
WriteLog("SELFTEST switchlink: not started, " + tool.status);
return selftest_result::handled;
}
tool.mouse = glm::dvec3{target.x, tool.point.y, target.z};
switch_link plan;
plan_switch_link(plan);
log_link(plan.link, plan.reason);
finish_switch_placement();
m_cursor_override.reset();
WriteLog("SELFTEST switchlink: " + tool.status);
return selftest_result::handled;
}
TTrack *own{nullptr};
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && false == track->m_editorremoved && track->eType == tt_Switch && track->m_paths.size() == 2 && geometry::plan_distance(track->m_paths[1].points[segment_data::point::end], point) < 3.0)
own = track;
if (own == nullptr)
{
WriteLog("SELFTEST branchlink: no switch branch end there");
return selftest_result::handled;
}
editor_track::snap_target end;
end.track = own;
end.point = {1, editor_track::point_kind::end};
end.position = own->m_paths[1].points[segment_data::point::end];
if (false == start_extend(end))
{
WriteLog("SELFTEST branchlink: the branch end isn't free");
return selftest_result::handled;
}
m_extend.pressed = {-10000.f, -10000.f};
m_extend.mouse = glm::dvec3{target.x, m_extend.point.y, target.z};
m_extend.snap = editor_track::find_free_end(own, own->iCategoryFlag & 15, m_extend.mouse, 3.0, {own});
editor_track::branch_link link;
std::string reason;
plan_branch_link(link, reason);
log_link(link, reason);
finish_extend();
WriteLog(format("SELFTEST branchlink: switch now %.3f %.3f -> %.3f %.3f", own->m_paths[0].points[segment_data::point::start].x, own->m_paths[0].points[segment_data::point::start].z, own->m_paths[0].points[segment_data::point::end].x,
own->m_paths[0].points[segment_data::point::end].z));
return selftest_result::handled;
}
if (command == "switchslide" || command == "switchreplace")
{
std::string name;

View File

@@ -105,6 +105,28 @@ double const kJointRange{400.0}; // the ends of the paths are drawn for the trac
ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)};
int const kRailCategory{1};
editor_track::branch_end branch_start(segment_data const &Diverging)
{
auto const tangent{bezier{Diverging}.first(1.0)};
return {Diverging.points[segment_data::point::end], plan_of(tangent), grade_of(tangent), static_cast<double>(Diverging.rolls[1])};
}
editor_track::branch_end branch_target(editor_track::snap_target const &Target)
{
return {Target.position, plan_of(Target.direction), grade_of(Target.direction), Target.track != nullptr ? editor_track::joint_roll(*Target.track, Target.point) : 0.0};
}
std::string link_summary(editor_track::snap_target const &Target, editor_track::branch_link const &Link)
{
auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}};
auto text{Link.radius > 0.0 ? format(STR_C("The branch leads into %s with an R %.0f m curve"), name.c_str(), Link.radius) : format(STR_C("The branch leads straight into %s"), name.c_str())};
if (std::abs(Link.shift) >= 0.005)
text += format(STR_C(", the switch moved %.2f m"), Link.shift);
if (Link.trim > 0.0)
text += format(STR_C(", %s shortened %.2f m"), name.c_str(), Link.trim);
return text;
}
std::optional<alignment::join_result> eased_join(alignment::join_request const &Request, editor_track::easing const &In, editor_track::easing const &Out)
{
if (In.pieces.empty() && Out.pieces.empty())
@@ -3724,11 +3746,17 @@ std::string editor_mode::track_readout() const
}
editor_track::snap_target target;
auto const snapped{extend_snap(target) && target.track != nullptr};
editor_track::branch_link link;
std::string reason;
if (plan_branch_link(link, reason))
return link_summary(target, link);
if (snapped && pieces.empty())
return "no track fits to the end of " + name(target.track);
auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)};
if (snapped)
text += "\njoins " + name(target.track);
if (false == reason.empty())
text += "\n" + reason;
return text + "\n" + track_shaping(snapped);
}
if (m_lay.active)
@@ -4063,6 +4091,20 @@ bool editor_mode::start_extend(editor_track::snap_target const &End)
m_extend.origin = End;
m_extend.pressed = {io.MousePos.x, io.MousePos.y};
m_extend.active = true;
if (track->eType == tt_Switch && End.point.path == 1 && End.point.kind == editor_track::point_kind::end)
{
double low{0.0};
double high{0.0};
std::string reason;
auto const axis{editor_track::main_axis(*track)};
if (axis.has_value() && switch_slides(*track) && switch_room(*track, low, high, reason))
{
m_extend.slides = true;
m_extend.slide_low = low;
m_extend.slide_high = high - axis->length;
}
m_extend.line = editor_track::find_straight(*track, m_straights.tolerance).tracks;
}
return true;
}
@@ -4301,6 +4343,13 @@ std::vector<segment_data> editor_mode::extend_pieces() const
editor_track::snap_target target;
if (extend_snap(target))
{
editor_track::branch_link link;
std::string reason;
if (plan_branch_link(link, reason))
{
m_join_last = {};
return link.pieces;
}
double length;
std::string error;
return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error);
@@ -4313,10 +4362,9 @@ std::vector<segment_data> editor_mode::extend_pieces() const
auto const begin{plan_of(curve.first(0.0))};
auto const finish{plan_of(curve.first(1.0))};
auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0};
if (std::abs(turn) > 1e-4)
if (auto const radius{editor_track::branch_radius(diverging)}; radius > 0.0)
{
int const side{turn > 0.0 ? 1 : -1};
auto const radius{curve.plan_length() / std::abs(turn)};
auto const &direction{tool.direction};
auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)};
auto const angle{geometry::signed_angle(plan_of(tool.point) - centre, plan_of(tool.mouse) - centre) * side};
@@ -4401,6 +4449,14 @@ void editor_mode::finish_extend()
m_track_point = m_extend.origin.point;
return;
}
std::string reason;
if (editor_track::branch_link link; plan_branch_link(link, reason))
{
finish_branch_link(link);
return;
}
if (false == reason.empty())
ui()->set_status(reason, true);
crossover_plan plan;
editor_track::snap_target target;
if (false == extend_snap(target) && plan_crossover(plan))
@@ -4924,6 +4980,11 @@ std::vector<segment_data> editor_mode::switch_preview() const
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side);
}
if (switch_link plan; plan_switch_link(plan))
{
plan.paths.insert(plan.paths.end(), plan.link.pieces.begin(), plan.link.pieces.end());
return plan.paths;
}
auto const &line{tool.line};
glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z};
auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1};
@@ -5005,9 +5066,16 @@ bool editor_mode::start_switch_placement()
void editor_mode::finish_switch_placement()
{
auto &tool{m_switch};
switch_link plan;
auto const linked{plan_switch_link(plan)};
tool.placing = false;
if (tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()))
return;
if (linked)
{
insert_linked_switch(plan);
return;
}
if (tool.curved)
{
auto const sample{sample_at(tool.frame, tool.along)};
@@ -5023,6 +5091,8 @@ void editor_mode::finish_switch_placement()
glm::dvec2 const direction{tool.line.direction * static_cast<double>(forward)};
auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1};
insert_switch(tool.line, tool.along, forward, side);
if (false == plan.reason.empty())
ui()->set_status(plan.reason, true);
}
void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side)
@@ -5276,6 +5346,11 @@ void editor_mode::draw_build_overlay() const
ImVec2 screen;
if (projection.project(target.position, screen))
drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f);
editor_track::branch_link link;
std::string reason;
if (plan_branch_link(link, reason) && std::abs(link.shift) >= 0.005)
for (auto const &path : editor_track::slid(*m_extend.track, link.shift))
drawpath(path, overlay_color::grip);
for (auto const &piece : extend_pieces())
drawpath(piece, overlay_color::marked);
}
@@ -5292,8 +5367,19 @@ void editor_mode::draw_build_overlay() const
}
}
if (m_switch.placing)
{
for (auto const &piece : switch_preview())
drawpath(piece, overlay_color::marked);
switch_link plan;
auto const linked{plan_switch_link(plan)};
ImVec2 screen;
if (plan.target.track != nullptr && projection.project(plan.target.position, screen))
{
drawlist->AddCircle(screen, 14.0f, linked ? IM_COL32(255, 60, 255, 255) : overlay_color::invalid, 20, 3.0f);
if (false == linked && false == plan.reason.empty())
drawlist->AddText({screen.x + 18.0f, screen.y - 8.0f}, overlay_color::invalid, plan.reason.c_str());
}
}
if (m_lay.active)
{
auto const &lay{m_lay};
@@ -5756,6 +5842,162 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const
return true;
}
std::string editor_mode::link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const
{
auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{"(noname)"}};
if (false == Link.valid)
return Link.shortfall > 0.0 ? format(STR_C("The branch can't lead into %s: the tracks are %.2f m too close for this switch with its curve"), name.c_str(), Link.shortfall) :
format(STR_C("The branch can't lead into %s"), name.c_str());
if (Link.trim > 0.0)
{
auto const &path{Target.track->m_paths[Target.point.path]};
if (has_controls(path) || geometry::chord_length(path) < Link.trim + 1.0)
return format(STR_C("%s would have to be shortened by %.2f m, it's too short or not straight"), name.c_str(), Link.trim);
}
return {};
}
bool editor_mode::plan_switch_link(switch_link &Plan) const
{
auto const &tool{m_switch};
if (false == tool.placing || tool.curved || tool.armed < 0 || tool.armed >= static_cast<int>(tool.templates.size()) || tool.line.tracks.empty() || tool.templates[tool.armed].double_slip)
return false;
auto const &shape{tool.templates[tool.armed]};
auto const &line{tool.line};
std::vector<TTrack const *> const exclude(line.tracks.begin(), line.tracks.end());
auto const category{line.tracks.front()->iCategoryFlag & 15};
auto const orient = [&](glm::dvec3 const &Toward) {
glm::dvec2 const offset{plan_of(Toward - tool.point)};
Plan.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1;
Plan.side = cross(line.direction * static_cast<double>(Plan.forward), offset) >= 0.0 ? 1 : -1;
};
auto const place = [&](double const Along) {
auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Along};
return editor_track::place_switch(shape, point, line.direction * static_cast<double>(Plan.forward), Plan.side, line.grade * Plan.forward);
};
auto const &io{ImGui::GetIO()};
if (false == m_cursor_override.has_value())
Plan.target = snap_free_end(tool.mouse, {io.MousePos.x, io.MousePos.y}, nullptr, category, exclude);
if (Plan.target.track == nullptr)
Plan.target = editor_track::find_free_end(nullptr, category, tool.mouse, kLaySnapRadius, exclude);
if (Plan.target.track != nullptr)
orient(Plan.target.position);
else
{
orient(tool.mouse);
auto const paths{place(tool.along)};
if (paths.size() < 2)
return false;
Plan.target = editor_track::find_free_end(nullptr, category, paths[1].points[segment_data::point::end], kLaySnapRadius, exclude);
if (Plan.target.track == nullptr)
return false;
}
auto const paths{place(tool.along)};
if (paths.size() < 2)
return false;
glm::dvec2 const direction{line.direction * static_cast<double>(Plan.forward)};
Plan.link = editor_track::link_branch(branch_start(paths[1]), {direction.x, line.grade * Plan.forward, direction.y}, editor_track::branch_radius(paths[1]), branch_target(Plan.target), glm::radians(m_joints.angle * 0.5));
Plan.reason = link_problem(Plan.target, Plan.link);
if (false == Plan.reason.empty())
return false;
Plan.along = tool.along + Plan.link.shift * Plan.forward;
auto const from{Plan.forward > 0 ? Plan.along : Plan.along - shape.length};
if (from < -1e-6 || from + shape.length > line.length + 1e-6)
{
Plan.reason = format(STR_C("The switch would have to move %.1f m, off the straight it stands on"), Plan.link.shift);
return false;
}
Plan.paths = place(Plan.along);
return true;
}
void editor_mode::insert_linked_switch(switch_link const &Plan)
{
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
std::vector<TTrack *> removed;
if (false == place_switch_on_straight(m_switch.line, m_switch.templates[m_switch.armed], nullptr, Plan.along, Plan.forward, Plan.side, false, states, created, removed, switch_name_for_new()))
return;
auto const placed{created};
link_tracks(Plan.target, Plan.link, states, created);
push_track_snapshot(std::move(states), std::move(created), std::move(removed));
straight_refresh();
m_switch.status.clear();
switch_placed(placed);
m_switch.status = link_summary(Plan.target, Plan.link) + (m_switch.status.empty() ? std::string{} : "\n" + m_switch.status);
ui()->set_status(m_switch.status, false);
}
bool editor_mode::plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const
{
auto const &tool{m_extend};
editor_track::snap_target target;
Reason.clear();
if (false == extend_snap(target) || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() != 2 || tool.path != 1 || false == tool.atend)
return false;
if (std::find(tool.line.begin(), tool.line.end(), target.track) != tool.line.end())
return false;
auto const axis{editor_track::main_axis(*tool.track)};
if (false == axis.has_value())
return false;
auto const &main{tool.track->m_paths.front()};
auto const &diverging{tool.track->m_paths[1]};
auto const rise{grade_of(main.points[segment_data::point::end] - main.points[segment_data::point::start])};
Link = editor_track::link_branch(branch_start(diverging), {axis->direction.x, rise, axis->direction.y}, editor_track::branch_radius(diverging), branch_target(target), glm::radians(m_joints.angle * 0.5));
Reason = link_problem(target, Link);
if (false == Reason.empty())
return false;
if (std::abs(Link.shift) >= 0.005 && (false == tool.slides || Link.shift < tool.slide_low - 1e-6 || Link.shift > tool.slide_high + 1e-6))
{
Reason = format(STR_C("The switch would have to move %.1f m, there's no plain track for it there"), Link.shift);
return false;
}
return true;
}
void editor_mode::finish_branch_link(editor_track::branch_link const &Link)
{
auto *own{m_extend.track};
auto const target{m_extend.snap};
m_extend.active = false;
auto link{Link};
bool const slides{std::abs(link.shift) >= 0.005};
if (slides && false == slide_switch(*own, link.shift))
return;
if (false == slides && false == link.pieces.empty())
link.pieces.front().points[segment_data::point::start] = own->m_paths[1].points[segment_data::point::end];
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
auto const linked{link_tracks(target, link, states, created)};
bool const changed{false == states.empty() || false == created.empty()};
push_track_snapshot(std::move(states), std::move(created));
if (slides && changed)
m_history.back().joined = true;
m_node = linked.empty() ? own : linked.back();
ui()->set_node(m_node);
straight_refresh();
ui()->set_status(link_summary(target, link), false);
}
std::vector<TTrack *> editor_mode::link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, std::vector<std::pair<TTrack *, editor_track::state>> &States, std::vector<TTrack *> &Created)
{
auto *target{Target.track};
if (Link.trim > 0.0)
{
States.emplace_back(target, editor_track::capture(*target));
auto const run{glm::length(plan_of(Target.direction))};
editor_track::move_point(*target, Target.point, Target.position + Target.direction * (Link.trim / std::max(run, 1e-6)));
editor_track::commit({target});
}
std::vector<TTrack *> pieces;
for (auto const &piece : Link.pieces)
pieces.push_back(editor_track::create_path(*target, piece));
editor_track::commit(pieces);
digitize_drawn(pieces);
Created.insert(Created.end(), pieces.begin(), pieces.end());
return pieces;
}
std::vector<TTrack *> editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style, std::string const &Type)
{
auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; };

View File

@@ -204,6 +204,27 @@ class editor_track
static std::optional<straight_axis> main_axis(TTrack const &Switch);
static std::vector<segment_data> slid(TTrack const &Switch, double const Distance);
struct branch_end
{
glm::dvec3 position{0.0};
glm::dvec2 direction{0.0, 1.0};
double grade{0.0};
double roll{0.0};
};
struct branch_link
{
bool valid{false};
double shift{0.0};
double trim{0.0};
double radius{0.0};
double before{0.0};
double after{0.0};
double shortfall{0.0};
std::vector<segment_data> pieces;
};
static double branch_radius(segment_data const &Diverging);
static branch_link link_branch(branch_end const &From, glm::dvec3 const &Slide, double const Radius, branch_end const &To, double const Kink);
struct snap_target
{
TTrack *track{nullptr};

View File

@@ -10,12 +10,14 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "editor/editorTrack.hpp"
#include "editor/editorGeometry.hpp"
#include "editor/editorGroundFollow.hpp"
#include "world/Track.h"
#include "utilities/Globals.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace
{
@@ -363,6 +365,116 @@ std::vector<segment_data> editor_track::slid(TTrack const &Switch, double const
return paths;
}
double editor_track::branch_radius(segment_data const &Diverging)
{
if (std::abs(Diverging.radius) > 0.5f)
return std::abs(Diverging.radius);
bezier const curve{Diverging};
auto const begin{plan_of(curve.first(0.0))};
auto const finish{plan_of(curve.first(1.0))};
if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9)
return 0.0;
auto const turn{std::abs(signed_angle(begin, finish))};
return turn > 1e-4 ? curve.plan_length() / turn : 0.0;
}
editor_track::branch_link editor_track::link_branch(branch_end const &From, glm::dvec3 const &Slide, double const Radius, branch_end const &To, double const Kink)
{
branch_link result;
if (glm::length(From.direction) < 1e-9 || glm::length(To.direction) < 1e-9 || glm::length(plan_of(Slide)) < 1e-9)
return result;
auto const slide{glm::normalize(plan_of(Slide))};
auto const rise{Slide.y / glm::length(plan_of(Slide))};
auto const tangent{glm::normalize(From.direction)};
auto const into{glm::normalize(To.direction)};
auto const turn{signed_angle(tangent, into)};
auto const start{plan_of(From.position)};
auto const target{plan_of(To.position)};
auto const across{cross(into, slide)};
if (std::abs(turn) > glm::radians(170.0))
return result;
std::vector<segment_data> arc;
glm::dvec2 chord{0.0};
if (std::abs(turn) > Kink)
{
if (Radius < 1.0)
return result;
arc = arc_pieces(glm::dvec3{0.0}, tangent, 0.0, Radius, std::abs(turn), turn > 0.0 ? 1 : -1);
chord = plan_of(arc.back().points[segment_data::point::end]);
result.radius = Radius;
}
else if (std::abs(across) < 1e-6)
return result;
if (false == arc.empty() && std::abs(across) < std::sin(glm::radians(1.0)))
{
auto const gap{target - start - chord};
auto const determinant{cross(tangent, slide)};
if (std::abs(determinant) < 1e-6)
return result;
result.before = cross(gap, slide) / determinant;
result.shift = cross(tangent, gap) / determinant;
if (result.before < -0.01)
{
result.shortfall = -result.before * std::abs(std::sin(turn));
return result;
}
result.before = std::max(0.0, result.before);
}
else
{
result.shift = cross(into, target - start - chord) / across;
result.after = glm::dot(target - (start + slide * result.shift + chord), into);
if (result.after < -0.01)
{
result.trim = -result.after;
result.after = 0.0;
}
}
auto const origin{glm::dvec3{start.x, From.position.y, start.y} + glm::dvec3{slide.x, rise, slide.y} * result.shift};
auto const line = [](glm::dvec2 const &A, glm::dvec2 const &B) {
segment_data path;
path.points[segment_data::point::start] = {A.x, 0.0, A.y};
path.points[segment_data::point::end] = {B.x, 0.0, B.y};
return path;
};
auto cursor{plan_of(origin)};
if (result.before >= 0.01)
{
result.pieces.push_back(line(cursor, cursor + tangent * result.before));
cursor += tangent * result.before;
}
for (auto piece : arc)
{
for (auto const index : {segment_data::point::start, segment_data::point::end})
piece.points[index] += glm::dvec3{cursor.x, 0.0, cursor.y};
result.pieces.push_back(piece);
}
cursor += chord;
if (result.after >= 0.01)
result.pieces.push_back(line(cursor, target));
else if (result.trim == 0.0 && false == result.pieces.empty())
result.pieces.back().points[segment_data::point::end] = {target.x, 0.0, target.y};
if (false == result.pieces.empty())
{
auto const height1{result.trim > 0.0 ? To.position.y + To.grade * result.trim : To.position.y};
ground_follow::follow(result.pieces, {origin.y, From.grade, height1, To.grade}, [](std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &) { return std::vector<double>(Points.size(), std::numeric_limits<double>::quiet_NaN()); });
std::vector<double> lengths;
double total{0.0};
for (auto const &piece : result.pieces)
total += lengths.emplace_back(bezier{piece}.plan_length());
double chainage{0.0};
for (std::size_t i = 0; i < result.pieces.size(); ++i)
{
auto const from{From.roll + (To.roll - From.roll) * chainage / std::max(total, 1e-6)};
chainage += lengths[i];
auto const to{From.roll + (To.roll - From.roll) * chainage / std::max(total, 1e-6)};
result.pieces[i].rolls = {static_cast<float>(from), static_cast<float>(to)};
}
}
result.valid = true;
return result;
}
void editor_track::edit::capture(TTrack &Track)
{
if (std::find(changed.begin(), changed.end(), &Track) != changed.end())

View File

@@ -5455,3 +5455,30 @@ msgstr "Zwrotnica %s: napęd %s, teraz po lewej"
msgid "Switch %s without a drive now"
msgstr "Zwrotnica %s jest teraz bez napędu"
msgid "The branch leads into %s with an R %.0f m curve"
msgstr "Odgałęzienie wchodzi w %s łukiem R %.0f m"
msgid "The branch leads straight into %s"
msgstr "Odgałęzienie wchodzi w %s prostą"
msgid ", the switch moved %.2f m"
msgstr ", rozjazd przesunięty o %.2f m"
msgid ", %s shortened %.2f m"
msgstr ", %s skrócony o %.2f m"
msgid "The branch can't lead into %s: the tracks are %.2f m too close for this switch with its curve"
msgstr "Odgałęzienie nie wejdzie w %s: tory są o %.2f m za blisko dla tego rozjazdu z łukiem"
msgid "The branch can't lead into %s"
msgstr "Odgałęzienie nie wejdzie w %s"
msgid "%s would have to be shortened by %.2f m, it's too short or not straight"
msgstr "%s trzeba by skrócić o %.2f m, a jest za krótki albo nie jest prosty"
msgid "The switch would have to move %.1f m, off the straight it stands on"
msgstr "Rozjazd musiałby się przesunąć o %.1f m, poza prostą, na której stoi"
msgid "The switch would have to move %.1f m, there's no plain track for it there"
msgstr "Rozjazd musiałby się przesunąć o %.1f m, a tam nie ma zwykłego toru"