/* 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 #include #include 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 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(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 inserts; std::vector 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(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::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 const &Switches, std::vector 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(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(i))) { Kind = static_cast(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(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(Request.forward)}; auto const origin{from.point(Request.along)}; std::vector 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 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{}; }; double reached{0.0}; for (std::size_t i = 0; i < offsets.size(); ++i) { bool const last{i + 1 == offsets.size()}; std::vector paths; std::vector 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(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(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