diff --git a/CMakeLists.txt b/CMakeLists.txt index c63147cc..1a7dc151 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -183,6 +183,8 @@ set(SOURCES "editor/editorAlignmentStore.cpp" "editor/editorSwitchStore.cpp" "editor/editorSwitchDesign.cpp" +"editor/editorConnections.cpp" +"application/editormodeconnection.cpp" "editor/editorSpeed.cpp" "editor/editorProfile.cpp" "editor/editorGauge.cpp" diff --git a/application/editormode.h b/application/editormode.h index 89a6a411..acd413eb 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorTerrainStreamer.hpp" #include "editor/editorOrthophoto.hpp" #include "editor/editorTrack.hpp" +#include "editor/editorConnections.hpp" #include "editor/editorAlignment.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" @@ -998,8 +999,22 @@ class editor_mode : public application_mode, private editor_track::observer }; slide_state slide; double slide_typed{0.0}; + int connection{-1}; + int connection_second{-1}; + double connection_radius{0.0}; + double connection_gap{6.0}; + bool connection_together{false}; + char connection_spacings[64]{"4.75;4.75;5.0"}; + double connection_stub{25.0}; }; switch_tool m_switch; + bool plan_connection(connection::request &Request, connection::result &Result) const; + void insert_connection(); + void render_connection_ui(); + std::string connection_readout(connection::result const &Plan) const; + editor_track::straight straight_near(glm::dvec3 const &Point, std::vector const &Exclude, double const Reach) const; + editor_track::straight straight_through(glm::dvec3 const &Point, glm::dvec2 const &Direction) const; + void record_connection(connection::kind const Kind, std::vector const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape); bool switch_room(TTrack &Track, double &Low, double &High, std::string &Reason); bool switch_vacate(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Shift, editor_track::straight const &Line, editor_track::edit &Edit, std::string &Status); bool move_switch_paths(TTrack &Track, std::vector const &Paths, std::string &Status); diff --git a/application/editormodeconnection.cpp b/application/editormodeconnection.cpp new file mode 100644 index 00000000..73f2513b --- /dev/null +++ b/application/editormodeconnection.cpp @@ -0,0 +1,388 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editormodeshared.h" +#include "editor/editorFormat.hpp" +#include "editor/editorAlignmentStore.hpp" +#include "editor/editorSwitchStore.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "world/Track.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include +#include +#include + +namespace +{ + +using geometry::cross; +using geometry::plan_of; + +char const *connection_name(int const Kind) +{ + switch (static_cast(Kind)) + { + case connection::kind::double_crossover: return STR_C("Double crossover"); + case connection::kind::scissors: return STR_C("Scissors crossover"); + case connection::kind::ladder: return STR_C("Switch ladder"); + default: return STR_C("Crossover"); + } +} + +std::vector spacings_of(char const *Text) +{ + std::string text{Text}; + for (auto &letter : text) + if (letter == ';') + letter = ' '; + else if (letter == ',') + letter = '.'; + std::istringstream words{text}; + std::vector result; + double value{0.0}; + while (words >> value) + result.push_back(value); + return result; +} + +std::string switch_label(editor_track::switch_template const &Template) +{ + if (Template.source != nullptr) + return STR("Scenery: ") + Template.label; + return (Template.own ? STR("Own: ") : std::string{"PLK: "}) + Template.label; +} + +bool usable_in_connection(editor_track::switch_template const &Template) +{ + return false == Template.double_slip && false == Template.twofold(); +} + +} // namespace + +editor_track::straight editor_mode::straight_near(glm::dvec3 const &Point, std::vector const &Exclude, double const Reach) const +{ + TTrack *found{nullptr}; + auto best{Reach}; + for (auto *section : simulation::Region->sections(Point, static_cast(std::max(40.0, Reach)))) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + { + if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || std::find(Exclude.begin(), Exclude.end(), track) != Exclude.end() || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const segment{plan_of(path.points[segment_data::point::end]) - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(Point), a + segment * t)}; + if (distance < best) + { + best = distance; + found = track; + } + } + return found != nullptr ? editor_track::find_straight(*found, m_straights.tolerance) : editor_track::straight{}; +} + +editor_track::straight editor_mode::straight_through(glm::dvec3 const &Point, glm::dvec2 const &Direction) const +{ + for (auto *section : simulation::Region->sections(Point, 40.f)) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + { + if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const segment{plan_of(path.points[segment_data::point::end]) - a}; + auto const length{glm::length(segment)}; + if (length < 1e-6 || std::abs(cross(segment / length, Direction)) > 1e-3 || std::abs(cross(segment / length, plan_of(Point) - a)) > 0.02) + continue; + auto const t{glm::dot(plan_of(Point) - a, segment) / (length * length)}; + if (t >= -1e-6 && t <= 1.0 + 1e-6) + return editor_track::find_straight(*track, m_straights.tolerance); + } + return {}; +} + +bool editor_mode::plan_connection(connection::request &Request, connection::result &Result) const +{ + auto const &tool{m_switch}; + Result = {}; + if (false == tool.placing || tool.curved || tool.connection < 0 || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size()) || tool.line.tracks.empty()) + return false; + auto const &line{tool.line}; + Request = {}; + Request.type = static_cast(tool.connection); + Request.from = connection::of(line); + Request.first = tool.templates[tool.armed]; + auto const second{tool.connection_second >= 0 && tool.connection_second < static_cast(tool.templates.size()) ? tool.connection_second : tool.armed}; + Request.second = tool.templates[second]; + Request.radius = tool.connection_radius; + Request.gap = tool.connection_gap; + Request.together = tool.connection_together; + Request.stub = tool.connection_stub; + Request.spacings = spacings_of(tool.connection_spacings); + Request.along = tool.along; + glm::dvec2 const offset{plan_of(tool.mouse - tool.point)}; + Request.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1; + glm::dvec2 const travel{line.direction * static_cast(Request.forward)}; + Request.side = cross(travel, offset) >= 0.0 ? 1 : -1; + std::vector const exclude(line.tracks.begin(), line.tracks.end()); + auto const other{straight_near(tool.mouse, exclude, 8.0)}; + if (false == other.tracks.empty()) + { + auto const target{connection::of(other)}; + if (Request.type != connection::kind::ladder) + { + Request.to.push_back(target); + Request.target = target.along(tool.mouse); + } + else if (std::abs(cross(target.direction, line.direction)) < 1e-3) + { + auto const side{cross(travel, plan_of(target.point(target.along(tool.point)) - tool.point)) > 0.0 ? 1.0 : -1.0}; + auto const lateral = [&](glm::dvec3 const &Point) { return side * cross(travel, plan_of(Point - tool.point)); }; + auto const outermost{lateral(target.point(target.along(tool.point)))}; + std::map> fan; + auto const better = [](double const One, double const Two) { return (One < 0.0) != (Two < 0.0) ? One >= 0.0 : std::abs(One) < std::abs(Two); }; + auto const centre{(tool.point + tool.mouse) * 0.5}; + for (auto *section : simulation::Region->sections(centre, static_cast(glm::length(offset) * 0.5 + 200.0))) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + { + if (track->eType != tt_Normal || track->m_editorremoved || track->m_paths.empty() || std::find(exclude.begin(), exclude.end(), track) != exclude.end() || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{path.points[segment_data::point::start]}; + auto const b{path.points[segment_data::point::end]}; + auto const direction{plan_of(b - a)}; + if (glm::length(direction) < 1e-6 || std::abs(cross(glm::normalize(direction), line.direction)) > 1e-3) + continue; + auto const distance{lateral(a)}; + auto const along1{glm::dot(travel, plan_of(a - tool.point))}; + auto const along2{glm::dot(travel, plan_of(b - tool.point))}; + if (distance < 1.0 || distance > outermost + 0.05 || std::max(along1, along2) <= 0.0) + continue; + auto const nearer{std::min(along1, along2)}; + auto &entry{fan[std::lround(distance * 20.0)]}; + if (entry.second == nullptr || better(nearer, entry.first)) + entry = {nearer, track}; + } + for (auto const &entry : fan) + Request.to.push_back(connection::of(editor_track::find_straight(*entry.second.second, m_straights.tolerance))); + } + } + Result = connection::plan(Request); + return Result.valid; +} + +std::string editor_mode::connection_readout(connection::result const &Plan) const +{ + if (false == Plan.valid) + return Plan.error; + auto text{format(STR_C("%s: %d switches, spacing %.2f m"), connection_name(m_switch.connection), static_cast(Plan.switches.size()), Plan.spacing)}; + if (false == Plan.straights.empty()) + { + text += STR_C(", straight inserts"); + for (auto const length : Plan.straights) + text += format(" %.2f", length); + text += " m"; + } + if (Plan.points > 0.0) + text += format(STR_C(", mathematical points %.2f m apart"), Plan.points); + if (Plan.radius > 0.0) + text += format(STR_C(", aligning curve R %.0f m"), Plan.radius); + if (Plan.crossing && Plan.angle > 1e-6) + text += format(STR_C(", crossing 1:%.2f"), 1.0 / std::tan(Plan.angle)); + if (false == Plan.fan.empty()) + { + text += STR_C(", tracks at"); + for (auto const offset : Plan.fan) + text += format(" %.2f", offset); + text += " m"; + } + if (false == Plan.warning.empty()) + text += "\n" + Plan.warning; + return text; +} + +void editor_mode::insert_connection() +{ + auto &tool{m_switch}; + connection::request request; + connection::result plan; + auto const planned{plan_connection(request, plan)}; + tool.placing = false; + if (false == planned) + { + tool.status = plan.error.empty() ? std::string{STR_C("Drag onto the other track")} : plan.error; + ui()->set_status(tool.status, true); + return; + } + std::vector> states; + std::vector created; + std::vector removed; + auto *style{tool.line.tracks.front()}; + auto const fail = [&](std::string const &Reason) { + for (auto *track : created) + editor_track::retire(*track); + for (auto entry{states.rbegin()}; entry != states.rend(); ++entry) + { + editor_track::apply(*entry->first, entry->second); + editor_track::commit({entry->first}); + } + for (auto *track : removed) + { + track->m_editorremoved = false; + editor_track::commit({track}); + } + if (auto before{switch_store::take_changes()}) + switch_store::restore(std::move(*before)); + if (auto before{alignment_store::take_changes()}) + alignment_store::restore(std::move(*before)); + straight_refresh(); + tool.status = Reason; + ui()->set_status(Reason, true); + }; + std::vector switches; + for (auto const &entry : plan.switches) + { + auto const &shape{entry.second ? request.second : request.first}; + auto const name{switch_name_for_new()}; + auto const before{created.size()}; + auto const start{entry.paths[0].points[segment_data::point::start]}; + auto const end{entry.paths[0].points[segment_data::point::end]}; + if (entry.line >= 0) + { + auto const &target{entry.line == 0 ? request.from : request.to[entry.line - 1]}; + auto const found{straight_through((start + end) * 0.5, target.direction)}; + if (found.tracks.empty()) + { + fail(STR_C("A switch of the connection goes where its track isn't a plain straight")); + return; + } + auto const forward{glm::dot(found.direction, plan_of(end - start)) > 0.0 ? 1 : -1}; + if (false == place_switch_on_straight(found, shape, nullptr, found.along(start), forward, entry.side, false, states, created, removed, name)) + { + fail(tool.status); + return; + } + } + else + { + auto const made{create_switch_set(shape, entry.paths, *style, name)}; + if (made.empty()) + { + fail(tool.status); + return; + } + created.insert(created.end(), made.begin(), made.end()); + } + auto const made{std::find_if(created.begin() + static_cast(before), created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (made == created.end() || glm::distance((*made)->m_paths.front().points[segment_data::point::start], start) > 0.01) + { + fail(STR_C("A switch of the connection doesn't fit where it was planned, its straight is too short")); + return; + } + switches.push_back(*made); + advance_switch_name(); + } + std::vector inserts; + for (auto const &insert : plan.inserts) + if (auto *track{editor_track::create_path(*style, insert)}) + inserts.push_back(track); + editor_track::commit(inserts); + digitize_drawn(inserts); + created.insert(created.end(), inserts.begin(), inserts.end()); + if (plan.crossing) + { + auto const first{glm::normalize(plan_of(plan.ends[1] - plan.ends[0]))}; + auto second{glm::normalize(plan_of(plan.ends[3] - plan.ends[2]))}; + if (glm::dot(first, second) < 0.0) + second = -second; + auto const parts{build_double_slip(plan.ends[0], plan.ends[1], plan.ends[2], plan.ends[3], plan_of(plan.centre), plan.centre.y, first, second, plan.angle, 0.0, *style, "Skrz")}; + editor_track::commit(parts); + created.insert(created.end(), parts.begin(), parts.end()); + } + record_connection(request.type, switches, plan, request.first); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + first_step_done(first_step::turnout); + m_track_point = {}; + m_node = switches.front(); + ui()->set_node(m_node); + std::string names; + for (auto *track : switches) + { + if (place_switch_drive(*track)) + join_last_steps(); + names += (names.empty() ? "" : ", ") + track->name(); + } + tool.status = format(STR_C("%s laid, switches %s"), connection_name(tool.connection), names.c_str()) + "\n" + connection_readout(plan); + ui()->set_status(tool.status, false); +} + +void editor_mode::render_connection_ui() +{ + auto &tool{m_switch}; + ImGui::PushItemWidth(220.0f); + if (ImGui::BeginCombo(STR_C("Connection##switchconnection"), tool.connection < 0 ? STR_C("Single switch") : connection_name(tool.connection))) + { + if (ImGui::Selectable(STR_C("Single switch"), tool.connection < 0)) + tool.connection = -1; + for (int i = 0; i < connection::kind_count; ++i) + if (ImGui::Selectable(connection_name(i), tool.connection == i)) + tool.connection = i; + ImGui::EndCombo(); + } + item_tooltip("A connection of tracks laid whole: press on a straight and drag onto the other track. Its switches are cut into the tracks with the inserts between them and get their drives; Shift+wheel while dragging: another kind, Ctrl+wheel: R of the aligning curve"); + if (tool.connection < 0) + { + ImGui::PopItemWidth(); + return; + } + auto const label = [&](int const Index) { return Index < 0 || Index >= static_cast(tool.templates.size()) ? std::string{STR_C("The same as the first one")} : switch_label(tool.templates[Index]); }; + if (ImGui::BeginCombo(STR_C("Other switches##connectionsecond"), label(tool.connection_second).c_str())) + { + if (ImGui::Selectable(STR_C("The same as the first one"), tool.connection_second < 0)) + tool.connection_second = -1; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (usable_in_connection(tool.templates[i]) && ImGui::Selectable((switch_label(tool.templates[i]) + "##connectionsecond" + std::to_string(i)).c_str(), tool.connection_second == i)) + tool.connection_second = i; + ImGui::EndCombo(); + } + item_tooltip("Template of the switches on the other track and along a ladder; the first switch is the one chosen on the list above"); + ImGui::InputDouble(STR_C("Aligning curve R, m##connectionradius"), &tool.connection_radius, 10.0, 100.0, "%.0f"); + tool.connection_radius = std::clamp(tool.connection_radius, 0.0, 10000.0); + item_tooltip("Of the curve between switches of different angles or on tracks that aren't parallel, and at the end of a ladder; 0: the radius of the sharper switch"); + if (tool.connection == static_cast(connection::kind::double_crossover)) + { + ImGui::InputDouble(STR_C("Gap between the switches, m##connectiongap"), &tool.connection_gap, 0.5, 1.0, "%.2f"); + tool.connection_gap = std::clamp(tool.connection_gap, 0.0, 100.0); + item_tooltip("Straight between the starts of the two switches meeting on one track"); + ImGui::Checkbox(STR_C("The switches meet on this track##connectiontogether"), &tool.connection_together); + item_tooltip("Otherwise the two crossovers meet on the other track; Alt+wheel while dragging switches it"); + } + if (tool.connection == static_cast(connection::kind::ladder)) + { + ImGui::InputText(STR_C("Spacings, m##connectionspacings"), tool.connection_spacings, sizeof(tool.connection_spacings)); + item_tooltip("Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan"); + ImGui::InputDouble(STR_C("New tracks, m##connectionstub"), &tool.connection_stub, 5.0, 25.0, "%.0f"); + tool.connection_stub = std::clamp(tool.connection_stub, 1.0, 1000.0); + item_tooltip("Length of the new tracks of the fan where there are none to lead onto"); + } + ImGui::PopItemWidth(); +} diff --git a/application/editormoderecords.cpp b/application/editormoderecords.cpp index 84b8f76f..85501650 100644 --- a/application/editormoderecords.cpp +++ b/application/editormoderecords.cpp @@ -278,6 +278,26 @@ void editor_mode::record_double(TTrack &First, TTrack &Second, editor_track::swi switch_store::put(entry, alignment_store::layer_of(First)); } +void editor_mode::record_connection(connection::kind const Kind, std::vector const &Switches, connection::result const &Plan, editor_track::switch_template const &Shape) +{ + if (Switches.empty() || Switches.front()->eType != tt_Switch || Switches.front()->m_paths.size() < 2 || scene::Layers.empty()) + return; + auto entry{switch_entry(Shape)}; + entry.kind = "connection"; + entry.name = Switches.front()->name(); + for (auto const *track : Switches) + if (track->name().empty()) + return; + else + entry.parts.push_back(track->name()); + switch_store::place(entry, *Switches.front()); + switch_store::classify(entry, *Switches.front()); + entry.type = connection::word(Kind); + entry.a = Plan.spacing; + entry.b = Plan.straights.empty() ? 0.0 : Plan.straights.front(); + switch_store::put(entry, alignment_store::layer_of(*Switches.front())); +} + std::optional editor_mode::double_of(TTrack const &Track) const { auto const of_switch = [](TTrack const &Switch) -> std::optional { diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 6a3cdd22..c1e26f66 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -1317,6 +1317,107 @@ editor_mode::selftest_result editor_mode::selftest_switch(std::string const &com WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status); return selftest_result::handled; } + if (command == "connection" || command == "connectionplan") + { + std::string kindword; + std::string label; + std::string second; + glm::dvec3 at{0.0}; + glm::dvec3 toward{0.0}; + words >> kindword >> at.x >> at.z >> toward.x >> toward.z >> label >> second; + connection::kind kind{connection::kind::crossover}; + if (false == connection::parse(kindword, kind)) + { + WriteLog("SELFTEST connection: unknown kind " + kindword); + return selftest_result::handled; + } + auto &tool{m_switch}; + for (std::string key; words >> key;) + { + if (key == "radius") + words >> tool.connection_radius; + else if (key == "gap") + words >> tool.connection_gap; + else if (key == "together") + words >> tool.connection_together; + else if (key == "stub") + words >> tool.connection_stub; + else if (key == "spacings") + { + std::string value; + words >> value; + std::snprintf(tool.connection_spacings, sizeof(tool.connection_spacings), "%s", value.c_str()); + } + } + show_track_tab(track_tab::turnout); + arm_switch(); + auto const find = [&](std::string Label) { + std::replace(Label.begin(), Label.end(), '_', ' '); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (false == Label.empty() && Label != "-" && false == tool.templates[i].double_slip && false == tool.templates[i].twofold() && tool.templates[i].label.find(Label) != std::string::npos) + return i; + return -1; + }; + auto const first{find(label)}; + if (first < 0) + { + WriteLog("SELFTEST connection: no template " + label); + return selftest_result::handled; + } + tool.armed = first; + tool.connection_second = find(second); + tool.connection = static_cast(kind); + auto const line{straight_near(at, {}, 3.0)}; + if (line.tracks.empty()) + { + WriteLog("SELFTEST connection: no straight at the start"); + return selftest_result::handled; + } + m_node = line.tracks.front(); + ui()->set_node(m_node); + m_cursor_override = at; + auto const started{start_switch_placement()}; + m_cursor_override.reset(); + if (false == started) + { + WriteLog("SELFTEST connection: not started, " + tool.status); + return selftest_result::handled; + } + tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; + connection::request request; + connection::result plan; + auto const valid{plan_connection(request, plan)}; + WriteLog("SELFTEST connection " + kindword + ": " + (valid ? connection_readout(plan) : "invalid, " + plan.error)); + for (auto const &entry : plan.switches) + { + auto const &start{entry.paths[0].points[segment_data::point::start]}; + auto const &end{entry.paths[1].points[segment_data::point::end]}; + WriteLog(format("SELFTEST connection switch on %d at %.3f, forward %d side %d%s: %.3f %.3f %.3f -> branch end %.3f %.3f %.3f", entry.line, entry.along, entry.forward, entry.side, entry.second ? " (second)" : "", start.x, start.y, start.z, end.x, end.y, + end.z)); + } + for (auto const &insert : plan.inserts) + { + auto const &start{insert.points[segment_data::point::start]}; + auto const &end{insert.points[segment_data::point::end]}; + WriteLog(format("SELFTEST connection insert %.3f %.3f %.3f -> %.3f %.3f %.3f, %.3f m, R %.1f", start.x, start.y, start.z, end.x, end.y, end.z, geometry::bezier{insert}.plan_length(), std::abs(insert.radius))); + } + if (command == "connectionplan" || false == valid) + { + tool.placing = false; + return selftest_result::handled; + } + insert_connection(); + WriteLog("SELFTEST connection built: " + tool.status); + for (auto const &entry : switch_store::records()) + if (entry.kind == "connection") + { + std::string parts; + for (auto const &part : entry.parts) + parts += ' ' + part; + WriteLog(format("SELFTEST connection record %s %s: spacing %.3f, insert %.3f, parts%s", entry.type.c_str(), entry.name.c_str(), entry.a, entry.b, parts.c_str())); + } + return selftest_result::handled; + } if (command == "conecheck") { std::string name; diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 1ec9166e..d8248101 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -3863,7 +3863,9 @@ std::vector editor_mode::track_key_hints(bool const All) else if (m_extend.active) hints = {{"Drag", "lays the shape to the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "another shape"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_switch.placing) - hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; + hints = m_switch.connection >= 0 && false == m_switch.curved ? + std::vector{{"Drag onto the other track", "the connection there"}, {"Shift+wheel", "another kind"}, {"Ctrl+wheel", "R of the aligning curve"}, {"Alt+wheel", "where a double crossover meets"}, {"Esc", "cancel"}} : + std::vector{{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; else if (m_track_tab == track_tab::lay) hints = {{"LMB", "start, on the ground or at a free end"}}; else if (m_track_tab == track_tab::turnout && (selected_track() == nullptr || selected_track()->eType == tt_Switch)) @@ -4823,6 +4825,25 @@ void editor_mode::render_layout_combo() bool editor_mode::track_wheel(double const Offset) { auto const &io{ImGui::GetIO()}; + if (m_switch.placing && m_switch.connection >= 0 && Offset != 0.0 && (io.KeyCtrl || io.KeyShift || io.KeyAlt)) + { + auto &tool{m_switch}; + int const step{Offset > 0.0 ? 1 : -1}; + if (io.KeyShift) + tool.connection = (tool.connection + step + connection::kind_count) % connection::kind_count; + else if (io.KeyCtrl) + { + static double const radii[]{0.0, 150.0, 190.0, 250.0, 300.0, 400.0, 500.0, 600.0, 800.0, 1000.0, 1200.0, 1500.0, 2000.0, 3000.0}; + int index{0}; + for (int i = 0; i < static_cast(std::size(radii)); ++i) + if (radii[i] <= tool.connection_radius + 1e-6) + index = i; + tool.connection_radius = radii[std::clamp(index + step, 0, static_cast(std::size(radii)) - 1)]; + } + else + tool.connection_together = false == tool.connection_together; + return true; + } if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) return false; bool const laying{m_lay.active && false == m_lay.points.empty()}; @@ -5416,6 +5437,18 @@ bool editor_mode::start_switch_placement() void editor_mode::finish_switch_placement() { auto &tool{m_switch}; + if (tool.connection >= 0) + { + if (false == tool.curved) + { + insert_connection(); + return; + } + tool.placing = false; + tool.status = STR_C("A connection is laid between straights"); + ui()->set_status(tool.status, true); + return; + } switch_link plan; auto const linked{plan_switch_link(plan)}; tool.placing = false; @@ -5765,7 +5798,40 @@ void editor_mode::draw_build_overlay() const drawpath(piece, overlay_color::marked); } } - if (m_switch.placing) + if (m_switch.placing && m_switch.connection >= 0 && false == m_switch.curved) + { + connection::request request; + connection::result plan; + auto const valid{plan_connection(request, plan)}; + if (valid) + { + for (auto const &entry : plan.switches) + for (auto const &path : entry.paths) + drawpath(path, overlay_color::marked); + for (auto const &insert : plan.inserts) + drawpath(insert, overlay_color::grip); + if (plan.crossing) + for (int i = 0; i < 4; i += 2) + { + segment_data path; + path.points[segment_data::point::start] = plan.ends[i]; + path.points[segment_data::point::end] = plan.ends[i + 1]; + drawpath(path, overlay_color::grip); + } + } + else + for (auto const &piece : switch_preview()) + drawpath(piece, overlay_color::invalid); + ImVec2 screen; + auto const text{valid ? connection_readout(plan) : plan.error.empty() ? std::string{STR_C("Drag onto the other track")} : plan.error}; + if (projection.project(m_switch.mouse, screen)) + { + auto const size{ImGui::CalcTextSize(text.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x + 14.0f, screen.y + 10.0f), ImVec2(screen.x + 22.0f + size.x, screen.y + 14.0f + size.y), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x + 18.0f, screen.y + 12.0f), valid ? (plan.warning.empty() ? overlay_color::highlight : overlay_color::warning) : overlay_color::invalid, text.c_str()); + } + } + else if (m_switch.placing) { for (auto const &piece : switch_preview()) drawpath(piece, overlay_color::marked); @@ -5892,6 +5958,7 @@ void editor_mode::render_switch_ui() list(true); } } + render_connection_ui(); ImGui::PushItemWidth(120.0f); ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name)); ImGui::PopItemWidth(); @@ -6164,9 +6231,7 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const if (std::abs(lateral) < 0.01) return false; auto const side{lateral > 0.0 ? 1.0 : -1.0}; - auto const normal{perpendicular * side}; - auto const offset{std::abs(lateral)}; - auto const &control{diverging.points[segment_data::point::control2]}; + auto const &control{(diverging.pieces.empty() ? diverging : diverging.pieces.back()).points[segment_data::point::control2]}; auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))}; auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))}; if (angle < 1e-4) @@ -6204,22 +6269,6 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const auto const &line{Plan.target}; if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) return false; - auto const distance{glm::dot(plan_of(line.start) - plan_of(origin), normal)}; - auto const remaining{distance - 2.0 * offset}; - if (remaining < 0.01) - return false; - auto const length{remaining / std::sin(angle)}; - glm::dvec2 const crossing{axis * std::cos(angle) + normal * std::sin(angle)}; - auto const alongfrog{glm::dot(plan_of(frog - origin), axis)}; - auto const otherfrog{plan_of(frog) + crossing * length}; - auto const otherorigin{otherfrog + axis * alongfrog + normal * offset}; - Plan.along = glm::dot(otherorigin - plan_of(line.start), line.direction); - if (Plan.along < 0.0 || Plan.along > line.length) - return false; - Plan.direction = glm::dot(-axis, line.direction) > 0.0 ? 1 : -1; - glm::dvec2 const direction{line.direction * static_cast(Plan.direction)}; - Plan.side = glm::dot(glm::dvec2{-direction.y, direction.x}, -normal) > 0.0 ? 1 : -1; - editor_track::switch_template shape; shape.source = tool.track; shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); @@ -6238,11 +6287,14 @@ bool editor_mode::plan_crossover(crossover_plan &Plan) const } } Plan.shape = shape; - auto const station{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Plan.along}; - Plan.paths = editor_track::place_switch(shape, station, direction, Plan.side, line.grade * Plan.direction); - Plan.insert = segment_data{}; - Plan.insert.points[segment_data::point::start] = frog; - Plan.insert.points[segment_data::point::end] = Plan.paths[1].points[segment_data::point::end]; + auto const faced{connection::face(frog, tangent, connection::of(line), 1, shape)}; + if (false == faced.valid) + return false; + Plan.along = faced.partner.along; + Plan.direction = faced.partner.forward; + Plan.side = faced.partner.side; + Plan.paths = faced.partner.paths; + Plan.insert = faced.insert; return true; } @@ -6265,7 +6317,7 @@ bool editor_mode::stretchable(editor_track::snap_target const &Target) const 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(tool.templates.size()) || tool.line.tracks.empty() || tool.templates[tool.armed].double_slip) + if (false == tool.placing || tool.curved || tool.connection >= 0 || tool.armed < 0 || tool.armed >= static_cast(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}; diff --git a/editor/editorConnections.cpp b/editor/editorConnections.cpp new file mode 100644 index 00000000..4b12df6e --- /dev/null +++ b/editor/editorConnections.cpp @@ -0,0 +1,626 @@ +/* +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 diff --git a/editor/editorConnections.hpp b/editor/editorConnections.hpp new file mode 100644 index 00000000..5165061b --- /dev/null +++ b/editor/editorConnections.hpp @@ -0,0 +1,107 @@ +/* +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/. +*/ + +#pragma once + +#include +#include +#include + +#include "editor/editorTrack.hpp" + +// connections of the straight tracks of a station made of switches: a crossover of two switches joined by a straight insert or +// by an aligning curve between straight inserts, a double crossover of two crossovers one after the other, a scissors crossover +// of two crossovers around one centre with a crossing between their inserts, a ladder of switches leading onto a fan of tracks +namespace connection +{ + +enum class kind +{ + crossover, + double_crossover, + scissors, + ladder +}; +int constexpr kind_count{4}; +char const *word(kind const Kind); +bool parse(std::string const &Word, kind &Kind); + +struct line +{ + glm::dvec3 start{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + double length{0.0}; + glm::dvec3 point(double const Along) const; + double along(glm::dvec3 const &Point) const; + // to the left of the direction + double lateral(glm::dvec3 const &Point) const; +}; +line of(editor_track::straight const &Straight); + +struct placed +{ + int line{0}; // 0 the starting track, 1 the other one, or the tracks of a fan in their order from 1, -1 the ladder track + double along{0.0}; // of the start of the switch along its line + int forward{1}; + int side{1}; + bool second{false}; // made of the template of the other tracks + std::vector paths; +}; + +// the switch on a track facing the end of a diverging track and the straight insert joining the two +struct facing +{ + bool valid{false}; + placed partner; + segment_data insert; + double length{0.0}; + std::string error; +}; +facing face(glm::dvec3 const &End, glm::dvec2 const &Heading, line const &To, int const Index, editor_track::switch_template const &Shape); + +struct request +{ + kind type{kind::crossover}; + line from; // the track the connection starts on + std::vector to; // the other track; for a ladder the tracks of the fan, the nearest first, or none + double along{0.0}; // of the start of the first switch on the starting track + int forward{1}; + int side{0}; // of a ladder with no tracks of the fan to lead onto + double target{0.0}; // where the switch on the other track starts along it when no straight insert fixes the place + editor_track::switch_template first; + editor_track::switch_template second; // on the other tracks and along the ladder + double radius{0.0}; // of the aligning curves and of the end of a ladder, 0: of the sharper switch + double insert{6.0}; // the shortest straight between curves turning the opposite ways + double gap{6.0}; // of a double crossover: between the starts of the switches meeting on one track + bool together{false}; // of a double crossover: the switches meet on the starting track, not on the other one + std::vector spacings; // of a ladder with no tracks of the fan: from the starting track to the first one and on + double stub{25.0}; // of a ladder: length of the new tracks of the fan +}; + +struct result +{ + bool valid{false}; + std::string error; + std::string warning; + std::vector switches; + std::vector inserts; + std::vector straights; // lengths of the straight inserts between the switches + double spacing{0.0}; // of the tracks at the start of the first switch + double points{0.0}; // between the mathematical points of the switches of the first crossover + double radius{0.0}; // of the aligning curve, 0 when a straight insert joins the switches + std::vector fan; // offsets of the tracks of a ladder from the starting track + bool crossing{false}; // of a scissors crossover, at the meeting of its two inserts + std::array ends{}; // of the two inserts crossing: along the first one, then along the second one + glm::dvec3 centre{0.0}; + double angle{0.0}; +}; +result plan(request const &Request); + +} // namespace connection diff --git a/editor/editorSwitchStore.cpp b/editor/editorSwitchStore.cpp index f004c24c..a624d1ca 100644 --- a/editor/editorSwitchStore.cpp +++ b/editor/editorSwitchStore.cpp @@ -156,7 +156,7 @@ void load() std::string km; std::size_t count{0}; line >> entry.kind; - if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing" && entry.kind != "double") + if (entry.kind != "switch" && entry.kind != "slip" && entry.kind != "crossing" && entry.kind != "double" && entry.kind != "connection") continue; line >> entry.name >> entry.type >> entry.rail >> entry.radius >> entry.ratio >> hand >> entry.a >> entry.b >> entry.main_radius >> keyword >> count; if (line.fail() || keyword != "parts" || count > 64) @@ -205,11 +205,24 @@ std::optional named(TTrack const &Track) if (name.empty()) return std::nullopt; for (auto const &entry : g_records) - if (std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end()) + if (entry.kind != "connection" && std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end()) return entry; return std::nullopt; } +std::vector connections(TTrack const &Track) +{ + load(); + std::vector result; + auto const &name{Track.name()}; + if (name.empty()) + return result; + for (auto const &entry : g_records) + if (entry.kind == "connection" && std::find(entry.parts.begin(), entry.parts.end(), name) != entry.parts.end()) + result.push_back(entry); + return result; +} + std::optional of(TTrack const &Track) { auto const found{named(Track)}; diff --git a/editor/editorSwitchStore.hpp b/editor/editorSwitchStore.hpp index 3917c3a9..09f1e606 100644 --- a/editor/editorSwitchStore.hpp +++ b/editor/editorSwitchStore.hpp @@ -26,7 +26,7 @@ namespace switch_store struct record { scene::layer_handle layer{null_handle}; - std::string kind{"switch"}; // switch, slip, crossing, double + std::string kind{"switch"}; // switch, slip, crossing, double, connection std::string name; // the switch, or the base name of a slip or a crossing std::string type{"Rz"}; // Rz, Rlj, Rld, Rkp, Rkpd, Skrz, Rpd, Rpj, - when unknown std::string rail{"-"}; @@ -55,6 +55,8 @@ std::vector const &records(); std::optional of(TTrack const &Track); // the record of the track by its name alone, also when the switch lost its shape std::optional named(TTrack const &Track); +// the connections of switches the switch is a part of +std::vector connections(TTrack const &Track); // records whose switches were moved or turned as a whole take their new place and km; returns their names std::vector follow(); // radius of the diverging track: of the type, bent with the main track for an arc switch diff --git a/lang/editor_pl.po b/lang/editor_pl.po index ef0f7786..d580f1ad 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -6010,3 +6010,168 @@ msgstr "Część toru zwrotnego jest krótsza niż 5 cm" msgid "A radius of the diverging track is under 10 m" msgstr "Promień toru zwrotnego jest mniejszy niż 10 m" +msgid "%s laid, switches %s" +msgstr "Ułożono: %s, zwrotnice %s" + +msgid "%s: %d switches, spacing %.2f m" +msgstr "%s: zwrotnic %d, rozstaw %.2f m" + +msgid ", aligning curve R %.0f m" +msgstr ", łuk wyrównujący R %.0f m" + +msgid ", crossing 1:%.2f" +msgstr ", skrzyżowanie 1:%.2f" + +msgid ", mathematical points %.2f m apart" +msgstr ", punkty matematyczne co %.2f m" + +msgid ", straight inserts" +msgstr ", wstawki proste" + +msgid ", tracks at" +msgstr ", tory w odległości" + +msgid "A connection is laid between straights" +msgstr "Połączenie układa się między prostymi" + +msgid "A connection is made of ordinary switches, not of slips, crossings or double switches" +msgstr "Połączenie składa się z rozjazdów zwyczajnych, nie z krzyżowych, skrzyżowań ani podwójnych" + +msgid "A connection of tracks laid whole: press on a straight and drag onto the other track. Its switches are cut into the tracks with the inserts between them and get their drives; Shift+wheel while dragging: another kind, Ctrl+wheel: R of the aligning curve" +msgstr "Połączenie torów układane w całości: wciśnij na prostej i przeciągnij na drugi tor. Zwrotnice są wcinane w tory razem ze wstawkami między nimi i dostają napędy; Shift+kółko podczas przeciągania: inny rodzaj, Ctrl+kółko: R łuku wyrównującego" + +msgid "A scissors crossover needs parallel tracks and switches of the same angle" +msgstr "Nożyce wymagają torów równoległych i rozjazdów o tym samym skosie" + +msgid "A switch of the connection doesn't fit where it was planned, its straight is too short" +msgstr "Zwrotnica połączenia nie mieści się tam, gdzie ją zaplanowano, jej prosta jest za krótka" + +msgid "A switch of the connection goes where its track isn't a plain straight" +msgstr "Zwrotnica połączenia wypada tam, gdzie tor nie jest zwykłą prostą" + +msgid "A switch of the connection runs off the end of its straight" +msgstr "Zwrotnica połączenia wychodzi poza koniec swojej prostej" + +msgid "Aligning curve R, m##connectionradius" +msgstr "R łuku wyrównującego, m##connectionradius" + +msgid "Alt+wheel" +msgstr "Alt+kółko" + +msgid "Connection##switchconnection" +msgstr "Połączenie##switchconnection" + +msgid "Crossover" +msgstr "Przejście rozjazdowe" + +msgid "Distances between the tracks of the fan from this track on, such as 4.75;4.75;5.0; used when the drag doesn't end on a parallel track, otherwise the parallel tracks up to that one make the fan" +msgstr "Odległości między torami wachlarza od tego toru, np. 4.75;4.75;5.0; używane, gdy przeciąganie nie kończy się na torze równoległym, w przeciwnym razie wachlarz tworzą tory równoległe aż do tamtego" + +msgid "Double crossover" +msgstr "Podwójne przejście rozjazdowe" + +msgid "Drag onto the other track" +msgstr "Przeciągnij na drugi tor" + +msgid "Gap between the switches, m##connectiongap" +msgstr "Odstęp między zwrotnicami, m##connectiongap" + +msgid "Give the spacings of the tracks of the ladder" +msgstr "Podaj rozstawy torów ulicy zwrotnicowej" + +msgid "Length of the new tracks of the fan where there are none to lead onto" +msgstr "Długość nowych torów wachlarza tam, gdzie nie ma torów do połączenia" + +msgid "New tracks, m##connectionstub" +msgstr "Nowe tory, m##connectionstub" + +msgid "Of the curve between switches of different angles or on tracks that aren't parallel, and at the end of a ladder; 0: the radius of the sharper switch" +msgstr "Łuku między rozjazdami o różnych skosach lub na torach nierównoległych oraz na końcu ulicy zwrotnicowej; 0: promień ostrzejszego rozjazdu" + +msgid "Other switches##connectionsecond" +msgstr "Pozostałe zwrotnice##connectionsecond" + +msgid "Otherwise the two crossovers meet on the other track; Alt+wheel while dragging switches it" +msgstr "W przeciwnym razie oba przejścia spotykają się na drugim torze; Alt+kółko podczas przeciągania to przełącza" + +msgid "R of the aligning curve" +msgstr "R łuku wyrównującego" + +msgid "Scissors crossover" +msgstr "Nożyce" + +msgid "Single switch" +msgstr "Pojedynczy rozjazd" + +msgid "Spacings, m##connectionspacings" +msgstr "Rozstawy, m##connectionspacings" + +msgid "Straight between the starts of the two switches meeting on one track" +msgstr "Prosta między początkami dwóch zwrotnic spotykających się na jednym torze" + +msgid "Switch ladder" +msgstr "Ulica zwrotnicowa" + +msgid "Template of the switches on the other track and along a ladder; the first switch is the one chosen on the list above" +msgstr "Szablon zwrotnic na drugim torze i wzdłuż ulicy zwrotnicowej; pierwsza zwrotnica to ta wybrana na liście powyżej" + +msgid "The aligning curve R %.0f m with %.0f m straights doesn't fit between the switches" +msgstr "Łuk wyrównujący R %.0f m z prostymi %.0f m nie mieści się między zwrotnicami" + +msgid "The diverging track runs along the other track" +msgstr "Tor zwrotny biegnie wzdłuż drugiego toru" + +msgid "The diverging tracks meet at %.2f deg, a straight insert can't join them" +msgstr "Tory zwrotne spotykają się pod kątem %.2f°, wstawka prosta ich nie połączy" + +msgid "The same as the first one" +msgstr "Taka sama jak pierwsza" + +msgid "The spacings of the tracks of a ladder have to be at least 1 m" +msgstr "Rozstawy torów ulicy zwrotnicowej muszą mieć co najmniej 1 m" + +msgid "The straight between the curves of the switches is %.2f m, under %.0f m" +msgstr "Prosta między łukami zwrotnic ma %.2f m, mniej niż %.0f m" + +msgid "The switch on the other track runs off the end of its straight" +msgstr "Zwrotnica na drugim torze wychodzi poza koniec swojej prostej" + +msgid "The switch template has no diverging track" +msgstr "Szablon rozjazdu nie ma toru zwrotnego" + +msgid "The switches can't face each other across these tracks" +msgstr "Zwrotnice nie mogą stanąć naprzeciw siebie na tych torach" + +msgid "The switches meet on this track##connectiontogether" +msgstr "Zwrotnice spotykają się na tym torze##connectiontogether" + +msgid "The switches of the two crossovers overlap, the gap between them has to be longer" +msgstr "Zwrotnice obu przejść nachodzą na siebie, odstęp między nimi musi być dłuższy" + +msgid "The tracks are %.2f m too close for these switches" +msgstr "Tory są o %.2f m za blisko dla tych rozjazdów" + +msgid "The tracks are too close for a scissors crossover with these switches, its switches overlap" +msgstr "Tory są za blisko dla nożyc z tymi rozjazdami, ich zwrotnice nachodzą na siebie" + +msgid "The tracks are too close for the crossing of a scissors crossover: %.2f m more spacing is needed" +msgstr "Tory są za blisko dla skrzyżowania nożyc: potrzeba o %.2f m większego rozstawu" + +msgid "The tracks of a fan have to be parallel, on one side and at least 1 m apart" +msgstr "Tory wachlarza muszą być równoległe, po jednej stronie i co najmniej 1 m od siebie" + +msgid "Track %d of the fan is %.2f m too close to the previous one for these switches" +msgstr "Tor %d wachlarza jest o %.2f m za blisko poprzedniego dla tych rozjazdów" + +msgid "Track %d of the fan reaches into the ladder, it has to start %.2f m further on" +msgstr "Tor %d wachlarza wchodzi w ulicę zwrotnicową, musi zaczynać się %.2f m dalej" + +msgid "another kind" +msgstr "inny rodzaj" + +msgid "the connection there" +msgstr "połączenie tam" + +msgid "where a double crossover meets" +msgstr "gdzie spotyka się podwójne przejście" +