17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-20 15:04:54 +02:00
Files
maszyna/editor/editorConnections.cpp

627 lines
22 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 "editor/editorConnections.hpp"
#include "editor/editorFormat.hpp"
#include "editor/editorGeometry.hpp"
#include "utilities/translation.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace connection
{
namespace
{
using geometry::bezier;
using geometry::cross;
using geometry::grade_of;
using geometry::has_controls;
using geometry::plan_of;
using geometry::signed_angle;
double constexpr kStraightTolerance{2e-3};
double constexpr kCrossingHalf{2.0};
struct branch
{
glm::dvec3 end{0.0};
glm::dvec2 heading{0.0, 1.0};
double grade{0.0};
double tail{0.0}; // straight at the end of the diverging track
};
bool plain(segment_data const &Piece)
{
if (false == has_controls(Piece))
return true;
bezier const curve{Piece};
auto const first{plan_of(curve.first(0.0))};
auto const last{plan_of(curve.first(1.0))};
if (glm::length(first) < 1e-9 || glm::length(last) < 1e-9)
return true;
return std::abs(signed_angle(first, last)) < 1e-6;
}
branch branch_of(std::vector<segment_data> const &Paths)
{
branch result;
if (Paths.size() < 2)
return result;
auto const &path{Paths[1]};
result.end = path.points[segment_data::point::end];
auto const &last{path.pieces.empty() ? path : path.pieces.back()};
auto tangent{bezier{last}.first(1.0)};
if (glm::length(plan_of(tangent)) < 1e-9)
tangent = last.points[segment_data::point::end] - last.points[segment_data::point::start];
result.heading = glm::normalize(plan_of(tangent));
result.grade = grade_of(tangent);
for (auto piece{path.pieces.rbegin()}; piece != path.pieces.rend() && plain(*piece); ++piece)
result.tail += bezier{*piece}.plan_length();
return result;
}
glm::dvec2 middle_point(placed const &Switch)
{
auto const &main{Switch.paths[0]};
auto const origin{plan_of(main.points[segment_data::point::start])};
auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - main.points[segment_data::point::start]))};
auto const diverging{branch_of(Switch.paths)};
auto const determinant{cross(diverging.heading, axis)};
if (std::abs(determinant) < 1e-9)
return origin;
return origin + axis * (cross(diverging.heading, plan_of(diverging.end) - origin) / determinant);
}
glm::dvec3 start_of(placed const &Switch)
{
return Switch.paths[0].points[segment_data::point::start];
}
bool fits(line const &Line, double const Along, int const Forward, double const Length)
{
auto const from{Forward > 0 ? Along : Along - Length};
return from >= -1e-3 && from + Length <= Line.length + 1e-3;
}
placed put(line const &Line, int const Index, double const Along, int const Forward, int const Side, editor_track::switch_template const &Shape, bool const Second)
{
placed result;
result.line = Index;
result.along = Along;
result.forward = Forward;
result.side = Side;
result.second = Second;
result.paths = editor_track::place_switch(Shape, Line.point(Along), Line.direction * static_cast<double>(Forward), Side, Line.grade * Forward);
return result;
}
segment_data straight(glm::dvec3 const &From, glm::dvec3 const &To)
{
segment_data path;
path.points[segment_data::point::start] = From;
path.points[segment_data::point::end] = To;
return path;
}
double sharper(editor_track::switch_template const &One, editor_track::switch_template const &Two)
{
double result{0.0};
for (auto const radius : {One.radius, Two.radius})
if (radius > 1.0 && (result == 0.0 || radius < result))
result = radius;
return result > 0.0 ? result : 190.0;
}
struct crossing_over
{
bool valid{false};
bool straight{false};
std::string error;
std::string warning;
placed one;
placed two;
std::vector<segment_data> inserts;
std::vector<double> straights;
double radius{0.0};
};
// the first switch on From diverging towards To and the switch on To facing it, joined by a straight insert when their angles
// agree, by an aligning curve between straight inserts when they don't, the switch on To then starting as near Target as it can
crossing_over cross_over(line const &From, int const Fromindex, double const Along, int const Forward, editor_track::switch_template const &Fromshape, bool const Fromsecond, line const &To, int const Toindex,
editor_track::switch_template const &Toshape, bool const Tosecond, double const Target, request const &Request)
{
crossing_over result;
if (false == fits(From, Along, Forward, Fromshape.length))
{
result.error = STR_C("A switch of the connection runs off the end of its straight");
return result;
}
glm::dvec2 const direction{From.direction * static_cast<double>(Forward)};
auto const origin{From.point(Along)};
auto const nearest{To.point(To.along(origin))};
auto const side{cross(direction, plan_of(nearest) - plan_of(origin)) > 0.0 ? 1 : -1};
result.one = put(From, Fromindex, Along, Forward, side, Fromshape, Fromsecond);
if (result.one.paths.size() < 2)
{
result.error = STR_C("The switch template has no diverging track");
return result;
}
auto const diverging{branch_of(result.one.paths)};
auto faced{face(diverging.end, diverging.heading, To, Toindex, Toshape)};
if (faced.valid)
{
faced.partner.second = Tosecond;
result.two = faced.partner;
result.straight = true;
if (faced.length >= 0.01)
result.inserts.push_back(faced.insert);
result.straights.push_back(faced.length);
auto const curves{faced.length + diverging.tail + branch_of(result.two.paths).tail};
if (curves < Request.insert - 1e-3)
result.warning = format(STR_C("The straight between the curves of the switches is %.2f m, under %.0f m"), curves, Request.insert);
result.valid = true;
return result;
}
if (faced.partner.paths.empty())
{
result.error = faced.error;
return result;
}
auto const forward{faced.partner.forward};
auto const otherside{faced.partner.side};
auto const radius{Request.radius > 0.0 ? Request.radius : sharper(Fromshape, Toshape)};
auto const trial{branch_of(put(To, Toindex, 0.0, forward, otherside, Toshape, Tosecond).paths)};
auto const into{-trial.heading};
auto const turn{signed_angle(diverging.heading, into)};
auto const determinant{cross(diverging.heading, into)};
if (std::abs(determinant) < 1e-9 || std::abs(turn) > glm::radians(60.0))
{
result.error = STR_C("The switches can't face each other across these tracks");
return result;
}
auto const tangent{radius * std::tan(std::abs(turn) * 0.5)};
auto const offset{plan_of(trial.end) - plan_of(diverging.end)};
auto const reach{cross(offset, into) / determinant};
auto const after{cross(diverging.heading, offset) / determinant};
auto const reachrate{cross(To.direction, into) / determinant};
auto const afterrate{cross(diverging.heading, To.direction) / determinant};
auto low{forward > 0 ? 0.0 : Toshape.length};
auto high{forward > 0 ? To.length - Toshape.length : To.length};
auto const bound = [&](double const Value, double const Rate, double const Need) {
if (std::abs(Rate) < 1e-12)
{
if (Value - tangent < Need - 1e-6)
{
low = std::numeric_limits<double>::infinity();
high = -low;
}
return;
}
auto const limit{(Need + tangent - Value) / Rate};
if (Rate > 0.0)
low = std::max(low, limit);
else
high = std::min(high, limit);
};
bound(reach, reachrate, std::max(0.0, Request.insert - diverging.tail));
bound(after, afterrate, std::max(0.0, Request.insert - trial.tail));
if (low > high + 1e-6)
{
result.error = format(STR_C("The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches"), radius, Request.insert);
return result;
}
result.two = put(To, Toindex, std::clamp(Target, low, std::max(low, high)), forward, otherside, Toshape, Tosecond);
auto const arrived{branch_of(result.two.paths)};
auto const link{editor_track::link_branch({diverging.end, diverging.heading, diverging.grade, 0.0}, radius, {arrived.end, -arrived.heading, -arrived.grade, 0.0}, 1e-4, Request.insert, false)};
if (false == link.valid || link.pieces.empty() || link.trim != 0.0 || link.reverse)
{
result.error = format(STR_C("The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches"), radius, Request.insert);
return result;
}
result.inserts = link.pieces;
result.straights = {link.before, link.after};
result.radius = radius;
result.valid = true;
return result;
}
void take(result &Result, crossing_over const &Part, bool const Inserts)
{
Result.switches.push_back(Part.one);
Result.switches.push_back(Part.two);
if (Inserts)
Result.inserts.insert(Result.inserts.end(), Part.inserts.begin(), Part.inserts.end());
Result.straights.insert(Result.straights.end(), Part.straights.begin(), Part.straights.end());
if (Part.radius > 0.0)
Result.radius = Part.radius;
if (Result.warning.empty())
Result.warning = Part.warning;
}
bool overlapping(std::vector<placed> const &Switches, std::vector<line> const &Lines)
{
for (std::size_t i = 0; i < Switches.size(); ++i)
for (std::size_t j = i + 1; j < Switches.size(); ++j)
{
auto const &one{Switches[i]};
auto const &two{Switches[j]};
if (one.line != two.line || one.line < 0 || one.line >= static_cast<int>(Lines.size()))
continue;
auto const &line{Lines[one.line]};
auto const span = [&](placed const &Switch) {
auto const a{line.along(Switch.paths[0].points[segment_data::point::start])};
auto const b{line.along(Switch.paths[0].points[segment_data::point::end])};
return std::pair{std::min(a, b), std::max(a, b)};
};
auto const [from1, to1]{span(one)};
auto const [from2, to2]{span(two)};
if (from2 < to1 - 0.01 && from1 < to2 - 0.01)
return true;
}
return false;
}
} // namespace
char const *word(kind const Kind)
{
switch (Kind)
{
case kind::double_crossover: return "double_crossover";
case kind::scissors: return "scissors";
case kind::ladder: return "ladder";
default: return "crossover";
}
}
bool parse(std::string const &Word, kind &Kind)
{
for (int i = 0; i < kind_count; ++i)
if (Word == word(static_cast<kind>(i)))
{
Kind = static_cast<kind>(i);
return true;
}
return false;
}
glm::dvec3 line::point(double const Along) const
{
return start + glm::dvec3{direction.x, grade, direction.y} * Along;
}
double line::along(glm::dvec3 const &Point) const
{
return glm::dot(plan_of(Point) - plan_of(start), direction);
}
double line::lateral(glm::dvec3 const &Point) const
{
return cross(direction, plan_of(Point) - plan_of(start));
}
line of(editor_track::straight const &Straight)
{
line result;
result.start = Straight.start;
result.direction = Straight.direction;
result.grade = Straight.grade;
result.length = Straight.length;
return result;
}
facing face(glm::dvec3 const &End, glm::dvec2 const &Heading, line const &To, int const Index, editor_track::switch_template const &Shape)
{
facing result;
auto const forward{glm::dot(To.direction, Heading) > 0.0 ? -1 : 1};
glm::dvec2 const direction{To.direction * static_cast<double>(forward)};
auto const side{cross(direction, plan_of(End) - plan_of(To.start)) > 0.0 ? 1 : -1};
auto const trial{put(To, Index, 0.0, forward, side, Shape, true)};
if (trial.paths.size() < 2)
{
result.error = STR_C("The switch template has no diverging track");
return result;
}
auto const diverging{branch_of(trial.paths)};
auto const mismatch{signed_angle(Heading, -diverging.heading)};
if (std::abs(mismatch) > kStraightTolerance)
{
result.partner = trial;
result.error = format(STR_C("The diverging tracks meet at %.2f deg, a straight insert can't join them"), glm::degrees(std::abs(mismatch)));
return result;
}
auto const across{cross(Heading, To.direction)};
if (std::abs(across) < 1e-9)
{
result.error = STR_C("The diverging track runs along the other track");
return result;
}
auto const shift{-cross(Heading, plan_of(diverging.end) - plan_of(End)) / across};
auto const meeting{plan_of(diverging.end) + To.direction * shift};
result.length = glm::dot(Heading, meeting - plan_of(End));
if (result.length < -0.005)
{
result.error = format(STR_C("The tracks are %.2f m too close for these switches"), -result.length * std::abs(across));
return result;
}
result.length = std::max(0.0, result.length);
if (false == fits(To, shift, forward, Shape.length))
{
result.error = STR_C("The switch on the other track runs off the end of its straight");
return result;
}
result.partner = put(To, Index, shift, forward, side, Shape, true);
result.insert = straight(End, result.partner.paths[1].points[segment_data::point::end]);
result.valid = true;
return result;
}
result plan(request const &Request)
{
result out;
auto const usable = [](editor_track::switch_template const &Shape) { return false == Shape.double_slip && false == Shape.twofold() && Shape.length > 0.5; };
if (false == usable(Request.first) || false == usable(Request.second))
{
out.error = STR_C("A connection is made of ordinary switches, not of slips, crossings or double switches");
return out;
}
auto const &from{Request.from};
glm::dvec2 const travel{from.direction * static_cast<double>(Request.forward)};
auto const origin{from.point(Request.along)};
std::vector<line> lines{from};
lines.insert(lines.end(), Request.to.begin(), Request.to.end());
if (Request.type == kind::ladder)
{
auto side{Request.side >= 0 ? 1 : -1};
std::vector<double> offsets;
if (false == Request.to.empty())
{
auto const &first{Request.to.front()};
side = cross(travel, plan_of(first.point(first.along(origin))) - plan_of(origin)) > 0.0 ? 1 : -1;
for (auto const &fan : Request.to)
{
auto const offset{side * cross(travel, plan_of(fan.point(fan.along(origin))) - plan_of(origin))};
if (offset < 1.0 || (false == offsets.empty() && offset < offsets.back() + 1.0) || std::abs(cross(fan.direction, from.direction)) > 1e-3)
{
out.error = STR_C("The tracks of a fan have to be parallel, on one side and at least 1 m apart");
return out;
}
offsets.push_back(offset);
}
}
else
{
double sum{0.0};
for (auto const spacing : Request.spacings)
{
if (spacing < 1.0)
{
out.error = STR_C("The spacings of the tracks of a ladder have to be at least 1 m");
return out;
}
offsets.push_back(sum += spacing);
}
if (offsets.empty())
{
out.error = STR_C("Give the spacings of the tracks of the ladder");
return out;
}
}
if (false == fits(from, Request.along, Request.forward, Request.first.length))
{
out.error = STR_C("A switch of the connection runs off the end of its straight");
return out;
}
auto const start{put(from, 0, Request.along, Request.forward, side, Request.first, false)};
auto const ladder{branch_of(start.paths)};
auto const climb{side * cross(travel, ladder.heading)};
if (climb < 1e-4)
{
out.error = STR_C("The switch template has no diverging track");
return out;
}
out.switches.push_back(start);
auto const lateral = [&](glm::dvec3 const &Point) { return side * cross(travel, plan_of(Point) - plan_of(origin)); };
glm::dvec3 const step{ladder.heading.x, ladder.grade, ladder.heading.y};
auto const rung = [&](double const Along) { return ladder.end + step * Along; };
auto const back{cross(ladder.heading, travel) > 0.0 ? 1 : -1};
auto const radius{Request.radius > 0.0 ? Request.radius : sharper(Request.second, Request.second)};
auto const settle = [&](glm::dvec3 const &Point, glm::dvec2 const &Heading, double const Grade) {
auto const turn{signed_angle(Heading, travel)};
return std::abs(turn) > kStraightTolerance ? geometry::arc_pieces(Point, Heading, Grade, radius, std::abs(turn), turn > 0.0 ? 1 : -1) : std::vector<segment_data>{};
};
double reached{0.0};
for (std::size_t i = 0; i < offsets.size(); ++i)
{
bool const last{i + 1 == offsets.size()};
std::vector<segment_data> paths;
std::vector<segment_data> bend;
glm::dvec3 landing{0.0};
if (false == last)
{
paths = editor_track::place_switch(Request.second, rung(0.0), ladder.heading, back, ladder.grade);
auto const diverging{branch_of(paths)};
bend = settle(diverging.end, diverging.heading, diverging.grade);
landing = bend.empty() ? diverging.end : bend.back().points[segment_data::point::end];
}
else
{
bend = settle(rung(0.0), ladder.heading, ladder.grade);
landing = bend.empty() ? rung(0.0) : bend.back().points[segment_data::point::end];
}
auto const along{(offsets[i] - lateral(landing)) / climb};
if (along < reached - 1e-3)
{
out.error = format(STR_C("Track %d of the fan is %.2f m too close to the previous one for these switches"), static_cast<int>(i + 1), (reached - along) * climb);
return out;
}
auto const shift{step * along};
if (along - reached >= 0.01)
{
out.inserts.push_back(straight(rung(reached), rung(along)));
out.straights.push_back(along - reached);
}
for (auto &path : paths)
path.translate(shift);
for (auto &piece : bend)
piece.translate(shift);
if (false == last)
{
placed rungswitch;
rungswitch.line = -1;
rungswitch.along = along;
rungswitch.side = back;
rungswitch.second = true;
rungswitch.paths = paths;
out.switches.push_back(rungswitch);
reached = along + Request.second.length;
}
if (false == bend.empty())
out.radius = radius;
out.inserts.insert(out.inserts.end(), bend.begin(), bend.end());
auto const fanstart{landing + shift};
if (i < Request.to.size())
{
auto const &fan{Request.to[i]};
auto end{fan.start};
auto ahead{glm::dot(travel, plan_of(fan.start) - plan_of(fanstart))};
auto const other{fan.point(fan.length)};
if (auto const further{glm::dot(travel, plan_of(other) - plan_of(fanstart))}; further < ahead)
{
end = other;
ahead = further;
}
if (ahead < -0.01)
{
out.error = format(STR_C("Track %d of the fan reaches into the ladder, it has to start %.2f m further on"), static_cast<int>(i + 1), -ahead);
return out;
}
if (ahead >= 0.01)
out.inserts.push_back(straight(fanstart, end));
}
else
out.inserts.push_back(straight(fanstart, fanstart + glm::dvec3{travel.x, from.grade * Request.forward, travel.y} * std::max(1.0, Request.stub)));
}
out.fan = offsets;
out.spacing = offsets.front();
out.valid = true;
return out;
}
if (Request.to.empty())
{
out.error = STR_C("Drag onto the other track");
return out;
}
auto const &to{Request.to.front()};
out.spacing = std::abs(to.lateral(origin));
auto const one{cross_over(from, 0, Request.along, Request.forward, Request.first, false, to, 1, Request.second, true, Request.target, Request)};
if (false == one.valid)
{
out.error = one.error;
return out;
}
out.points = glm::distance(middle_point(one.one), middle_point(one.two));
if (Request.type == kind::crossover)
{
take(out, one, true);
out.valid = true;
return out;
}
auto const towards{glm::dot(to.direction, travel) > 0.0 ? 1 : -1};
auto const span{glm::dot(travel, plan_of(start_of(one.two)) - plan_of(start_of(one.one)))};
if (Request.type == kind::double_crossover)
{
crossing_over two;
if (false == Request.together)
{
auto const along{one.two.along + towards * std::max(0.0, Request.gap)};
two = cross_over(to, 1, along, towards, Request.second, true, from, 0, Request.first, false, from.along(to.point(along)) + Request.forward * span, Request);
}
else
{
auto const along{Request.along - Request.forward * std::max(0.0, Request.gap)};
two = cross_over(from, 0, along, -Request.forward, Request.first, false, to, 1, Request.second, true, to.along(from.point(along)) - towards * span, Request);
}
if (false == two.valid)
{
out.error = two.error;
return out;
}
take(out, one, true);
take(out, two, true);
if (overlapping(out.switches, lines))
{
out.error = STR_C("The switches of the two crossovers overlap, the gap between them has to be longer");
return out;
}
out.valid = true;
return out;
}
if (false == one.straight || std::abs(cross(from.direction, to.direction)) > 1e-4)
{
out.error = STR_C("A scissors crossover needs parallel tracks and switches of the same angle");
return out;
}
auto const p1{branch_of(one.one.paths).end};
auto const p2{branch_of(one.two.paths).end};
auto const middle{(p1 + p2) * 0.5};
auto const two{cross_over(from, 0, 2.0 * from.along(middle) - Request.along, -Request.forward, Request.first, false, to, 1, Request.second, true, 0.0, Request)};
if (false == two.valid || false == two.straight)
{
out.error = two.valid ? std::string{STR_C("A scissors crossover needs parallel tracks and switches of the same angle")} : two.error;
return out;
}
take(out, one, false);
take(out, two, false);
if (overlapping(out.switches, lines))
{
out.error = STR_C("The tracks are too close for a scissors crossover with these switches, its switches overlap");
return out;
}
auto const q1{branch_of(two.one.paths).end};
auto const q2{branch_of(two.two.paths).end};
auto const first{plan_of(p2 - p1)};
auto const second{plan_of(q2 - q1)};
auto const determinant{cross(first, second)};
auto const sine{std::abs(cross(glm::normalize(travel), glm::length(first) > 1e-9 ? glm::normalize(first) : travel))};
auto const shortfall{(2.0 * kCrossingHalf - one.straights.front()) * sine};
if (std::abs(determinant) < 1e-9 || glm::length(first) < 2.0 * kCrossingHalf || glm::length(second) < 2.0 * kCrossingHalf)
{
out.error = format(STR_C("The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed"), std::max(0.01, shortfall));
return out;
}
auto const fraction{cross(plan_of(q1) - plan_of(p1), second) / determinant};
auto const meeting{plan_of(p1) + first * fraction};
auto const half{std::min({glm::distance(meeting, plan_of(p1)), glm::distance(meeting, plan_of(p2)), glm::distance(meeting, plan_of(q1)), glm::distance(meeting, plan_of(q2))})};
if (half < kCrossingHalf)
{
out.error = format(STR_C("The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed"), std::max(0.01, shortfall));
return out;
}
out.crossing = true;
out.ends = {p1, p2, q1, q2};
out.centre = {meeting.x, p1.y + (p2.y - p1.y) * fraction, meeting.y};
auto const unitone{glm::normalize(first)};
auto unittwo{glm::normalize(second)};
if (glm::dot(unitone, unittwo) < 0.0)
unittwo = -unittwo;
out.angle = std::atan2(std::abs(cross(unitone, unittwo)), glm::dot(unitone, unittwo));
out.valid = true;
return out;
}
} // namespace connection