/* 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: */ #include "stdafx.h" #include "application/editormode.h" #include "application/editoruilayer.h" #include "application/editorprojection.h" #include "application/editormodeshared.h" #include "editor/editorFormat.hpp" #include "editor/editorAlignmentStore.hpp" #include "editor/editorSwitchStore.hpp" #include "editor/editorGeometry.hpp" #include "editor/editorIncludeInfo.hpp" #include "utilities/Globals.h" #include "rendering/renderer.h" #include "world/Track.h" #include "world/TrackFeatures.h" #include "scene/scene.h" #include "scene/scenelayers.h" #include "simulation/simulation.h" #include "utilities/Logs.h" #include "utilities/translation.h" #include "imgui/imgui.h" #include "imgui/ImGuizmo.h" #include #include #include #include #include #include #include #include #include #include "application/editormodetrackshared.h" using namespace track_mode; std::vector editor_mode::switch_preview() const { auto const &tool{m_switch}; if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) return {}; if (tool.curved) { auto const sample{sample_at(tool.frame, tool.along)}; glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; glm::dvec2 const direction{sample.tangent * static_cast(forward)}; auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side); } if (switch_link plan; plan_switch_link(plan)) { plan.paths.insert(plan.paths.end(), plan.link.pieces.begin(), plan.link.pieces.end()); return plan.paths; } auto const &line{tool.line}; glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1}; glm::dvec2 const direction{line.direction * static_cast(forward)}; auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward); } bool editor_mode::start_switch_placement() { auto &tool{m_switch}; if (tool.armed < 0 || m_track_tab != track_tab::turnout) return false; // with no plain track selected, or a press away from it, the click selects auto *selected{selected_track()}; if (selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) return false; glm::dvec3 const ground{cursor_level(selected->m_paths.front().points[segment_data::point::start].y)}; tool.curved = false; tool.status.clear(); if (current_straight().tracks.empty()) { auto *track{selected}; if (tool.templates[tool.armed].double_slip) { tool.status = STR_C("A double slip goes only into a straight crossing another one"); return false; } editor_track::curve curve; editor_track::chain chain; std::string error; if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) { tool.status = STR_C("The selected track is neither a recognized straight nor a curve"); return false; } if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) { tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error; return false; } for (auto const &part : editor_track::chain_parts(chain)) if (std::find(part.tracks.begin(), part.tracks.end(), track) != part.tracks.end()) { chain = part; break; } tool.frame = sample_chain(chain); tool.frame_tracks = chain.tracks; if (tool.frame.size() < 2) return false; tool.curved = true; tool.along = nearest_station(tool.frame, ground); tool.point = sample_at(tool.frame, tool.along).position; if (plan_distance(tool.point, ground) > kSwitchReach) return false; tool.mouse = ground; tool.placing = true; return true; } auto const &line{current_straight()}; auto const cursor{cursor_on_straight(line)}; auto const along{std::clamp(line.along(cursor), 0.0, line.length)}; auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along}; if (plan_distance(point, cursor) > kSwitchReach) return false; if (tool.templates[tool.armed].double_slip) { insert_double_slip(line, cursor, tool.templates[tool.armed]); return true; } tool.along = along; tool.point = point; tool.mouse = cursor; tool.line = line; tool.placing = true; return true; } void editor_mode::finish_switch_placement() { auto &tool{m_switch}; if (tool.connection >= 0) { if (false == tool.curved) { insert_connection(); return; } insert_curved_connection(); return; } switch_link plan; auto const linked{plan_switch_link(plan)}; tool.placing = false; if (tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) return; if (linked) { insert_linked_switch(plan); return; } if (tool.curved) { auto const sample{sample_at(tool.frame, tool.along)}; glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; glm::dvec2 const direction{sample.tangent * static_cast(forward)}; auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; insert_curved_switch(forward, side); return; } glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1}; glm::dvec2 const direction{tool.line.direction * static_cast(forward)}; auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; insert_switch(tool.line, tool.along, forward, side); if (false == plan.reason.empty()) ui()->set_status(plan.reason, true); } void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side) { std::vector> states; std::vector created; std::vector removed; if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed, switch_name_for_new())) return; auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); switch_placed(placed); } void editor_mode::switch_placed(std::vector const &Created) { auto const found{std::find_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; if (found == Created.end()) return; auto const slip{std::count_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; }) > 1}; m_switch.placing = false; first_step_done(first_step::turnout); m_track_point = {}; m_node = *found; ui()->set_node(m_node); if (slip) { auto const known{switch_store::named(**found)}; if (false == known.has_value() || (known->kind != "double" && known->kind != "slip")) return; std::vector driven; for (auto *track : Created) if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) { join_last_steps(); driven.push_back(track->name()); } // a slip has a drive at each of its switches if (known->kind == "slip") { if (false == driven.empty()) { std::string names; for (auto const &name : driven) names += (names.empty() ? "" : ", ") + name; m_switch.status = format(STR_C("Slip with drives at its switches %s: the events of the name of a switch with + and - switch it, also in the map"), names.c_str()); ui()->set_status(m_switch.status, false); } return; } if (driven.size() == 2) { m_switch.status = format(STR_C("Double switch %s + %s with drives: events %s+, %s- and %s+, %s- switch them, also in the map"), driven[0].c_str(), driven[1].c_str(), driven[0].c_str(), driven[0].c_str(), driven[1].c_str(), driven[1].c_str()); ui()->set_status(m_switch.status, false); } advance_switch_name(); return; } if (place_switch_drive(**found)) join_last_steps(); advance_switch_name(); } void editor_mode::advance_switch_name() { if (false == m_switch.keep_names.empty()) { m_switch.keep_names.erase(m_switch.keep_names.begin()); return; } std::string name{m_switch.name}; auto digits{name.size()}; while (digits > 0 && std::isdigit(static_cast(name[digits - 1]))) --digits; if (digits < name.size() && name.size() - digits < 9) name = name.substr(0, digits) + std::to_string(std::stoi(name.substr(digits)) + 1); else name.clear(); std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str()); } std::string editor_mode::switch_name_for_new() const { if (false == m_switch.keep_names.empty() && simulation::Paths.find(m_switch.keep_names.front()) == nullptr) return m_switch.keep_names.front(); if (m_switch.name[0] != '\0') return m_switch.name; for (int i = 1;; ++i) { auto const name{"z" + std::to_string(i)}; if (simulation::Paths.find(name) == nullptr && simulation::Events.FindEvent(name + "+") == nullptr) return name; } } void editor_mode::join_last_steps() { if (m_history.size() < 2) return; auto &last{m_history.back()}; // a step of more than the includes placed, as the paths laid after others were taken out, is undone together with the one before auto const more{false == last.tracks.empty() || false == last.created.empty() || false == last.removed.empty() || false == last.directives.empty() || false == last.sweeps.empty() || false == last.sweeps_toggled.empty() || false == last.infra.empty() || last.alignments.has_value() || last.switch_records.has_value() || last.profiles || false == last.copies.empty() || last.terrain != nullptr}; if (more) { last.joined = true; return; } auto instances{std::move(last.instances)}; m_history.pop_back(); auto &previous{m_history.back()}; previous.instances.insert(previous.instances.end(), instances.begin(), instances.end()); } bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) { auto const from{From}; auto const to{To}; std::string reason; if (false == tracks_editable(Line.tracks, reason)) { m_switch.status = reason; ui()->set_status(reason, true); return false; } struct span { TTrack *track; int path; double from; double to; bool forward; }; std::vector spans; for (std::size_t i = 0; i < Line.tracks.size(); ++i) { auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; auto const a{Line.along(path.points[segment_data::point::start])}; auto const b{Line.along(path.points[segment_data::point::end])}; spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b}); } std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); int first{-1}; int last{-1}; for (int i = 0; i < static_cast(spans.size()); ++i) { if (spans[i].to > from + 1e-3 && spans[i].from < to - 1e-3) { if (first < 0) first = i; last = i; } } if (first < 0) return false; editor_track::chain chain; for (int i = first; i <= last; ++i) { if (spans[i].track->eType != tt_Normal) return false; chain.tracks.push_back(spans[i].track); chain.forward.push_back(spans[i].forward); } if (Style != nullptr) *Style = spans[first].track; auto const before{std::max(0.0, from - spans[first].from)}; auto const after{std::max(0.0, spans[last].to - to)}; bool const hasbefore{before > 0.01}; bool const hasafter{after > 0.01}; auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; if (false == hasbefore && false == hasafter) { for (auto *track : chain.tracks) editor_track::retire(*track); Removed.insert(Removed.end(), chain.tracks.begin(), chain.tracks.end()); return true; } auto const count{std::max(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))}; std::size_t countbefore{0}; if (hasbefore && hasafter) countbefore = std::clamp(static_cast(std::round(count * before / (before + after))), 1, count - 1); else if (hasbefore) countbefore = count; std::vector pieces; auto const straight = [&](double const Start, double const End, std::size_t const Count) { for (std::size_t i = 0; i < Count; ++i) { segment_data path; path.points[segment_data::point::start] = point(Start + (End - Start) * i / Count); path.points[segment_data::point::end] = point(Start + (End - Start) * (i + 1) / Count); pieces.push_back(path); } }; if (hasbefore) straight(spans[first].from, from, countbefore); if (hasafter) straight(to, spans[last].to, count - countbefore); for (auto *track : chain.tracks) States.emplace_back(track, editor_track::capture(*track)); auto const relaid{editor_track::relay(chain, pieces)}; Created.insert(Created.end(), relaid.begin(), relaid.end()); return true; } bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name) { auto const &shape{Shape}; if (shape.length > Line.length - 0.02) { m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length); return false; } auto const from{switch_from(Line, shape.length, Along, Direction, Snap)}; auto const to{from + shape.length}; auto const origin{Direction > 0 ? from : to}; TTrack *first{nullptr}; if (false == cut_straight(Line, from, to, States, Created, Removed, &first)) { m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track"); return false; } auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; glm::dvec2 const direction{Line.direction * static_cast(Direction)}; auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; auto const made{create_switch_set(shape, paths, Style != nullptr ? *Style : *first, Name)}; Created.insert(Created.end(), made.begin(), made.end()); return true; } std::vector editor_mode::create_switch_set(editor_track::switch_template const &Shape, std::vector const &Paths, TTrack const &Style, std::string const &Name) { if (Shape.twofold() && Paths.size() >= 5) { auto firstshape{Shape}; firstshape.length = glm::distance(Paths[0].points[segment_data::point::start], Paths[0].points[segment_data::point::end]); auto secondshape{Shape}; secondshape.length = Shape.length - Shape.stagger; auto *one{editor_track::create_switch(firstshape, Paths, Style, Name)}; auto *two{one != nullptr ? editor_track::create_switch(secondshape, {Paths[2], Paths[3]}, Style, Name + "b") : nullptr}; auto *link{two != nullptr ? editor_track::create_path(Style, Paths[4]) : nullptr}; if (link == nullptr) { m_switch.status = STR_C("The switch couldn't be created from this template"); return {}; } editor_track::commit({one, two, link}); record_double(*one, *two, Shape); m_switch.status = format(STR_C("Double switch %s with its second switch %s inserted"), one->name().c_str(), two->name().c_str()); return {one, two, link}; } auto *track{editor_track::create_switch(Shape, Paths, Style, Name)}; if (track == nullptr) { m_switch.status = STR_C("The switch couldn't be created from this template"); return {}; } editor_track::commit({track}); record_switch(*track, Shape); m_switch.status = format(STR_C("Switch %s inserted, Ctrl+Z takes it back"), track->name().c_str()); return {track}; } bool editor_mode::plan_crossover(crossover_plan &Plan) const { auto const &tool{m_extend}; if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || m_track_layout == track_layout::straight) return false; auto const &own{*tool.track}; auto const &main{own.m_paths[0]}; auto const &diverging{own.m_paths[1]}; auto const origin{main.points[segment_data::point::start]}; auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; glm::dvec2 const perpendicular{-axis.y, axis.x}; auto const &frog{diverging.points[segment_data::point::end]}; auto const lateral{glm::dot(plan_of(frog - origin), perpendicular)}; if (std::abs(lateral) < 0.01) return false; auto const side{lateral > 0.0 ? 1.0 : -1.0}; 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) return false; auto const ground{tool.mouse}; TTrack *target{nullptr}; double best{8.0}; auto const sections{simulation::Region->sections(ground, 40.f)}; for (auto *section : sections) { for (auto const &cell : section->m_cells) { for (auto *track : cell.m_paths) { if (track == &own || track->eType != tt_Normal || track->m_editorremoved || 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 b{plan_of(path.points[segment_data::point::end])}; auto const segment{b - a}; auto const t{std::clamp(glm::dot(plan_of(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; auto const distance{glm::distance(plan_of(ground), a + segment * t)}; if (distance < best) { best = distance; target = track; } } } } if (target == nullptr) return false; Plan.target = editor_track::find_straight(*target, m_straights.tolerance); auto const &line{Plan.target}; if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) return false; editor_track::switch_template shape; shape.source = tool.track; shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); for (int i = 0; i < 2; ++i) { auto const &path{own.m_paths[i]}; auto &local{shape.local[i]}; local = path; auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), perpendicular) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; local.transform([&](glm::dvec3 const &Point) { return tolocal(Point - origin); }, tolocal); if (side < 0.0) { local.rolls = {-local.rolls[0], -local.rolls[1]}; for (auto &piece : local.pieces) piece.rolls = {-piece.rolls[0], -piece.rolls[1]}; } } Plan.shape = shape; 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; } std::string editor_mode::link_problem(editor_track::snap_target const &Target, editor_track::branch_link const &Link) const { auto const name{Target.track != nullptr && false == Target.track->name().empty() ? Target.track->name() : std::string{STR_C("(no name)")}}; if (false == Link.valid) return Link.shortfall > 0.0 ? format(STR_C("The branch can't lead into %s: the R %.0f m curve needs %.2f m more room"), name.c_str(), Link.radius, Link.shortfall) : format(STR_C("The branch can't lead into %s"), name.c_str()); if (Link.trim > 0.0 && (false == stretchable(Target) || geometry::chord_length(Target.track->m_paths[Target.point.path]) < Link.trim + 1.0)) return format(STR_C("%s would have to be shortened by %.2f m, it's too short or not straight"), name.c_str(), Link.trim); return {}; } bool editor_mode::stretchable(editor_track::snap_target const &Target) const { return Target.track != nullptr && Target.track->eType == tt_Normal && Target.track->m_paths.size() == 1 && editor_track::is_straight(*Target.track, m_straights.tolerance); } bool editor_mode::plan_switch_link(switch_link &Plan) const { auto const &tool{m_switch}; 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}; std::vector const exclude(line.tracks.begin(), line.tracks.end()); auto const category{line.tracks.front()->iCategoryFlag & 15}; auto const orient = [&](glm::dvec3 const &Toward) { glm::dvec2 const offset{plan_of(Toward - tool.point)}; Plan.forward = glm::dot(offset, line.direction) >= 0.0 ? 1 : -1; Plan.side = cross(line.direction * static_cast(Plan.forward), offset) >= 0.0 ? 1 : -1; }; auto const place = [&](double const Along) { auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Along}; return editor_track::place_switch(shape, point, line.direction * static_cast(Plan.forward), Plan.side, line.grade * Plan.forward); }; auto const &io{ImGui::GetIO()}; if (false == m_cursor_override.has_value()) Plan.target = snap_free_end(tool.mouse, {io.MousePos.x, io.MousePos.y}, nullptr, category, exclude); if (Plan.target.track == nullptr) Plan.target = editor_track::find_free_end(nullptr, category, tool.mouse, kLaySnapRadius, exclude); if (Plan.target.track != nullptr) orient(Plan.target.position); else { orient(tool.mouse); auto const paths{place(tool.along)}; if (paths.size() < 2) return false; Plan.target = editor_track::find_free_end(nullptr, category, paths[1].points[segment_data::point::end], kLaySnapRadius, exclude); if (Plan.target.track == nullptr) return false; } if (shape.length > line.length - 0.02) return false; auto const kink{glm::radians(m_joints.angle * 0.5)}; auto const plan = [&](double const Along) { Plan.along = Along; Plan.paths = place(Along); if (Plan.paths.size() < 2) return false; auto const radius{editor_track::branch_radius(Plan.paths[1])}; Plan.link = editor_track::link_branch(branch_start(Plan.paths[1]), radius, branch_target(Plan.target), kink, editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(nullptr, Plan.target.track))), stretchable(Plan.target)); Plan.reason = link_problem(Plan.target, Plan.link); return Plan.reason.empty(); }; auto const from{switch_from(line, shape.length, tool.along, Plan.forward, true)}; auto const snapped{Plan.forward > 0 ? from : from + shape.length}; auto const paths{place(snapped)}; if (paths.size() < 2) return false; if (auto const shift{editor_track::branch_shift(branch_start(paths[1]), line.direction * static_cast(Plan.forward), editor_track::branch_radius(paths[1]), branch_target(Plan.target), kink)}) { auto const along{snapped + *shift * Plan.forward}; auto const begin{Plan.forward > 0 ? along : along - shape.length}; Plan.moved = std::abs(*shift); if (begin >= -1e-6 && begin + shape.length <= line.length + 1e-6 && plan(along) && false == Plan.link.reverse) return true; } Plan.moved = 0.0; return plan(snapped); } void editor_mode::insert_linked_switch(switch_link const &Plan) { std::vector> states; std::vector created; std::vector removed; if (false == place_switch_on_straight(m_switch.line, m_switch.templates[m_switch.armed], nullptr, Plan.along, Plan.forward, Plan.side, false, states, created, removed, switch_name_for_new())) return; auto const placed{created}; link_tracks(Plan.target, Plan.link, Plan.target.track->eType == tt_Normal ? *Plan.target.track : *m_switch.line.tracks.front(), states, created); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); m_switch.status.clear(); switch_placed(placed); auto summary{link_summary(Plan.target, Plan.link, editor_track::branch_radius(Plan.paths[1]))}; if (Plan.moved >= 0.005) summary += format(STR_C(", the switch moved %.2f m"), Plan.moved); m_switch.status = summary + (m_switch.status.empty() ? std::string{} : "\n" + m_switch.status); ui()->set_status(m_switch.status, false); } bool editor_mode::plan_branch_link(editor_track::branch_link &Link, std::string &Reason) const { auto const &tool{m_extend}; editor_track::snap_target target; Reason.clear(); if (false == extend_snap(target) || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() != 2 || tool.path != 1 || false == tool.atend) return false; if (std::find(tool.line.begin(), tool.line.end(), target.track) != tool.line.end()) return false; auto const &diverging{tool.track->m_paths[1]}; auto const radius{editor_track::branch_radius(diverging)}; Link = editor_track::link_branch(branch_start(diverging), radius, branch_target(target), glm::radians(m_joints.angle * 0.5), editor_track::reverse_insert(std::min(editor_track::diverging_speed(radius), design_speed(tool.track, target.track))), stretchable(target)); Reason = link_problem(target, Link); return Reason.empty(); } void editor_mode::finish_branch_link(editor_track::branch_link const &Link) { auto *own{m_extend.track}; auto const target{m_extend.snap}; m_extend.active = false; std::vector> states; std::vector created; auto const &style{target.track->eType == tt_Normal || m_extend.line.empty() ? *target.track : *m_extend.line.front()}; auto const linked{link_tracks(target, Link, style, states, created)}; push_track_snapshot(std::move(states), std::move(created)); m_node = linked.empty() ? own : linked.back(); ui()->set_node(m_node); straight_refresh(); ui()->set_status(link_summary(target, Link, editor_track::branch_radius(own->m_paths[1])), false); } std::vector editor_mode::link_tracks(editor_track::snap_target const &Target, editor_track::branch_link const &Link, TTrack const &Style, std::vector> &States, std::vector &Created) { auto *target{Target.track}; if (Link.trim != 0.0) { States.emplace_back(target, editor_track::capture(*target)); editor_track::move_point(*target, Target.point, Link.landing); auto &path{target->m_paths[Target.point.path]}; path.points[segment_data::point::control1] = glm::dvec3{0.0}; path.points[segment_data::point::control2] = glm::dvec3{0.0}; editor_track::commit({target}); } std::vector pieces; for (auto const &piece : Link.pieces) pieces.push_back(editor_track::create_path(Style, piece)); editor_track::commit(pieces); digitize_drawn(pieces); Created.insert(Created.end(), pieces.begin(), pieces.end()); return pieces; } std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, editor_track::slip_curve const &Curve, TTrack &Style, std::string const &Type) { auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; }; auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) { segment_data path; path.points[segment_data::point::start] = Start; path.points[segment_data::point::end] = End; return path; }; // the ends of the two diagonals at the crossing are kept apart further than the simulation joins the ends, 2 cm auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))}; auto const gap{std::max(0.25, 0.06 / halfsine)}; auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0}; auto const whole{Curve.length()}; // the part of the slip curve from Start along Heading, turning by Turn to Finish, between the distances From and To along it; // where it changes the radius made of pieces, with one curve from end to end for the versions which know no pieces auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) { auto const place = [&](double const Along) { auto const local{Curve.point(Along)}; auto const planar{plan_of(Start) + Heading * local.x + glm::dvec2{-Heading.y, Heading.x} * (Turn * local.y)}; return glm::dvec3{planar.x, Start.y + (Finish.y - Start.y) * Along / whole, planar.y}; }; auto const bend = [&](double const Begin, double const End, double const Radius, double const Handle0, double const Handle1) { auto const d0{turned(Heading, Turn * Curve.heading(Begin))}; auto const d3{turned(Heading, Turn * Curve.heading(End))}; segment_data path; path.points[segment_data::point::start] = place(Begin); path.points[segment_data::point::end] = place(End); if (Radius > 0.0) { path.points[segment_data::point::control1] = {d0.x * Handle0, 0.0, d0.y * Handle0}; path.points[segment_data::point::control2] = {-d3.x * Handle1, 0.0, -d3.y * Handle1}; } path.radius = static_cast(Radius); return path; }; std::vector> zones; for (auto const &zone : {std::pair{Curve.lead, 0.0}, {Curve.lead + Curve.ends, Curve.radius}, {Curve.lead + Curve.ends + Curve.between, Curve.middle}, {whole - Curve.lead, Curve.radius}, {whole, 0.0}}) { if (zone.first <= (zones.empty() ? 0.0 : zones.back().first) + 1e-9) continue; if (false == zones.empty() && zone.second == zones.back().second) zones.back().first = zone.first; else zones.push_back(zone); } // broken at the tips of the blades of the tables, so the model of the switch finds them there if (Curve.tip > 0.0) for (auto const cut : {Curve.tip, whole - Curve.tip}) for (std::size_t i = 0; i < zones.size(); ++i) if (cut > (i > 0 ? zones[i - 1].first : 0.0) + 0.05 && cut < zones[i].first - 0.05) { zones.insert(zones.begin() + i, {cut, zones[i].second}); break; } std::vector pieces; auto from{From}; for (auto const &[to, radius] : zones) { auto const upto{std::min(To, to)}; if (upto <= from + 1e-6) continue; auto const handle{radius > 0.0 ? 4.0 / 3.0 * std::tan((upto - from) / radius / 4.0) * radius : 0.0}; pieces.push_back(bend(from, upto, radius, handle, handle)); from = upto; } if (pieces.size() == 1) return pieces.front(); // the handles of the curve from end to end reach 2/3 of the way to the crossing of the tangents auto const chord{plan_of(place(To) - place(From))}; auto const d0{turned(Heading, Turn * Curve.heading(From))}; auto const d3{turned(Heading, Turn * Curve.heading(To))}; auto const sine{cross(d0, d3)}; auto path{bend(From, To, Curve.radius, 2.0 / 3.0 * cross(chord, d3) / sine, 2.0 / 3.0 * cross(d0, chord) / sine)}; path.pieces = std::move(pieces); return path; }; auto const back = [](segment_data const &Path) { auto result{reversed(Path)}; std::reverse(result.pieces.begin(), result.pieces.end()); for (auto &piece : result.pieces) piece = reversed(piece); return result; }; auto const middle{whole * 0.5}; auto const split{gap}; auto const centreA{at(Crossing - First * gap, Height)}; auto const centreB{at(Crossing + First * gap, Height)}; auto const centreC{at(Crossing - Second * gap, Height)}; auto const centreD{at(Crossing + Second * gap, Height)}; std::string base; auto const prefix{Type == "Skrz" ? "editor_skrz" : Type == "Rkp" ? "editor_dkp" : "editor_dks"}; // a crossing keeps the names of its tracks, only its record holds the name auto const recorded = [](std::string const &Name) { auto const &records{switch_store::records()}; return std::any_of(records.begin(), records.end(), [&](switch_store::record const &Record) { return Record.name == Name; }); }; for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr || simulation::Paths.find(base + "_d") != nullptr || recorded(base); ++i) base = prefix + std::to_string(i); std::vector parts; if (Type == "Skrz") { // the two tracks just cross, their ends far from the crossing parts.push_back(editor_track::create_path(Style, straight(PointA, PointB))); parts.push_back(editor_track::create_path(Style, straight(PointC, PointD))); parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); record_slip(parts, "crossing", Type, 0.0, Angle, base); return parts; } editor_track::switch_template shape; shape.length = glm::distance(plan_of(PointA), Crossing); bool const single{Type == "Rkp"}; parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a")); if (single) parts.push_back(editor_track::create_path(Style, straight(PointB, centreB))); else parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), back(arc(PointC, Second, -side, PointB, middle + split, whole))}, Style, base + "_b")); if (single) parts.push_back(editor_track::create_path(Style, straight(PointC, centreC))); else parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c")); parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), back(arc(PointA, First, side, PointD, middle + split, whole))}, Style, base + "_d")); parts.push_back(editor_track::create_path(Style, straight(centreA, centreB))); parts.push_back(editor_track::create_path(Style, straight(centreC, centreD))); parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split))); if (false == single) parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split))); parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); record_slip(parts, "slip", Type, Curve.radius, Angle, base, Curve.middle); return parts; } glm::dvec3 editor_mode::cursor_on_straight(editor_track::straight const &Line) const { // the height of the straight under the cursor, worked out anew till it settles: on a grade the plane of the start of the // straight would put the cursor metres away from where it points at the track auto cursor{cursor_level(Line.start.y)}; for (int i = 0; i < 4; ++i) cursor = cursor_level(Line.start.y + Line.grade * std::clamp(Line.along(cursor), 0.0, Line.length)); return cursor; } bool editor_mode::plan_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape, slip_plan &Plan) const { Plan = slip_plan{}; // the crossing of the straight with the rail track nearest the cursor, whatever that track is: the slip goes there or // nowhere, never to another crossing around TTrack *nearest{nullptr}; glm::dvec2 crossing{0.0}; double best{kSlipReach}; auto const category{Line.tracks.empty() ? 1 : Line.tracks.front()->iCategoryFlag & 15}; for (auto *section : simulation::Region->sections(Point, 300.f)) for (auto const &cell : section->m_cells) for (auto *track : cell.m_paths) { if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 15) != category || std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end()) continue; for (auto const &path : track->m_paths) { bezier const curve{path}; auto const straight{path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}}; int const pieces{straight ? 1 : 16}; auto previous{plan_of(path.points[segment_data::point::start])}; for (int i = 1; i <= pieces; ++i) { auto const next{straight ? plan_of(path.points[segment_data::point::end]) : plan_of(curve.point(static_cast(i) / pieces))}; auto const segment{next - previous}; auto const determinant{cross(Line.direction, segment)}; if (std::abs(determinant) > 1e-9) { auto const offset{previous - plan_of(Line.start)}; auto const along{cross(offset, segment) / determinant}; auto const fraction{cross(offset, Line.direction) / determinant}; auto const position{plan_of(Line.start) + Line.direction * along}; if (fraction >= -0.01 && fraction <= 1.01 && along >= 0.0 && along <= Line.length && glm::distance(position, plan_of(Point)) < best) { best = glm::distance(position, plan_of(Point)); nearest = track; crossing = position; } } previous = next; } } } // a double slip nearer the cursor than any crossing is replaced by the one chosen if (Shape.type == "Rkpd") { TTrack *slip{nullptr}; double closest{25.0}; 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_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan_of(track->location()), plan_of(Point)) < closest) { closest = glm::distance(plan_of(track->location()), plan_of(Point)); slip = track; } if (slip != nullptr && (nearest == nullptr || closest < best)) { Plan.replaces = slip; return true; } } if (nearest == nullptr) { Plan.problem = STR_C("No track crosses the selected straight under the cursor"); return false; } if (nearest->eType != tt_Normal) { Plan.problem = STR_C("The track crossing here is a switch: a slip goes only where two straights cross"); return false; } if (false == editor_track::is_straight(*nearest, m_straights.tolerance)) { Plan.problem = STR_C("The track crossing here isn't straight: a slip goes only where two straights cross"); return false; } auto const second{editor_track::find_straight(*nearest, m_straights.tolerance)}; if (second.tracks.empty()) { Plan.problem = STR_C("The track crossing here isn't straight: a slip goes only where two straights cross"); return false; } auto const first{Line.direction}; auto direction{second.direction}; if (glm::dot(first, direction) < 0.0) direction = -direction; auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; bool const plain{Shape.type == "Skrz"}; if (angle < glm::radians(0.5) || angle > glm::radians(plain ? 89.0 : 30.0)) { Plan.problem = STR_C("The straights cross at an angle unsuitable for a double slip"); return false; } auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)}; auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; auto const *gauged{Line.tracks.front()}; auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(gauged->fTrackWidth) + gauged->rail_dimensions().head + 0.002, blade.first, blade.second)}; curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); auto half{plain ? Shape.a : curve.tangent()}; if (plain && half > available) { half = available; if (half < 2.0) { Plan.problem = STR_C("Not enough straight track around the crossing"); return false; } } else if (half > available) { curve = curve.scaled(available / half); half = available; if (curve.radius < 50.0) { Plan.problem = STR_C("Not enough straight track around the crossing"); return false; } Plan.note = format(STR_C("R reduced to %d m, the straights end %d m from the crossing"), static_cast(curve.radius), static_cast(available)); } auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; }; auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0}; auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; }; Plan.other = nearest; Plan.crossing = crossing; Plan.first = first; Plan.direction = direction; Plan.angle = angle; Plan.alongfirst = alongfirst; Plan.half = half; Plan.curve = curve; Plan.points = {at(crossing - first * half, heightfirst(alongfirst - half)), at(crossing + first * half, heightfirst(alongfirst + half)), at(crossing - direction * half, heightsecond(alongsecond - sign * half)), at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; return true; } bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) { auto &state{m_straights}; state.status.clear(); slip_plan plan; if (false == plan_double_slip(Line, Point, Shape, plan)) { state.status = plan.problem; return false; } if (plan.replaces != nullptr) return replace_double_slip(*plan.replaces, Shape); state.status = plan.note; auto *other{plan.other}; auto const crossing{plan.crossing}; auto const first{plan.first}; auto const direction{plan.direction}; auto const angle{plan.angle}; auto const alongfirst{plan.alongfirst}; auto const half{plan.half}; auto const curve{plan.curve}; auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; auto const &pointA{plan.points[0]}; auto const &pointB{plan.points[1]}; auto const &pointC{plan.points[2]}; auto const &pointD{plan.points[3]}; std::vector> states; std::vector created; std::vector removed; TTrack *style{nullptr}; if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style)) return false; auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)}; auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)}; if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr)) { for (auto *track : created) editor_track::retire(*track); for (auto const &entry : states) { editor_track::apply(*entry.first, entry.second); editor_track::commit({entry.first}); } for (auto *track : removed) { editor_track::revive(*track); editor_track::commit({track}); } return false; } auto &styled{style != nullptr ? *style : *Line.tracks.front()}; // a single slip joins the pair of the tracks on the side of the click auto const nearer{glm::distance((plan_of(pointA) + plan_of(pointD)) * 0.5, plan_of(Point)) <= glm::distance((plan_of(pointC) + plan_of(pointB)) * 0.5, plan_of(Point))}; auto const parts{Shape.type == "Rkp" && false == nearer ? build_double_slip(pointC, pointD, pointA, pointB, crossing, heightfirst(alongfirst), direction, first, angle, curve, styled, Shape.type) : build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, curve, styled, Shape.type)}; editor_track::commit(parts); created.insert(created.end(), parts.begin(), parts.end()); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); switch_placed(parts); return true; } bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) { auto &state{m_straights}; auto const base{Part.name().substr(0, Part.name().size() - 2)}; std::array switches{}; for (int i = 0; i < 4; ++i) { switches[i] = simulation::Paths.find(base + "_" + static_cast('a' + i)); if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2) { state.status = STR_C("The double slip isn't complete, it can't be replaced"); return false; } } struct end { TTrack *track; glm::dvec3 point; glm::dvec2 inward; TTrack *outer; }; std::vector ends; std::vector parts(switches.begin(), switches.end()); for (auto *track : switches) { segment_data const *straight{nullptr}; for (auto const &path : track->m_paths) if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) straight = &path; if (straight == nullptr) straight = &track->m_paths.front(); auto const start{straight->points[segment_data::point::start]}; ends.push_back({track, start, glm::normalize(plan_of(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]}); for (int i = 0; i < 2; ++i) { auto *inner{track->SwitchExtension->pNexts[i]}; if (inner != nullptr && inner->eType == tt_Normal && std::find(parts.begin(), parts.end(), inner) == parts.end() && inner->Length() < 5.0) parts.push_back(inner); } } int partner{-1}; for (int i = 1; i < 4; ++i) if (glm::dot(ends[0].inward, ends[i].inward) < -0.999) partner = i; if (partner < 0) { state.status = STR_C("The double slip geometry isn't recognized"); return false; } auto const &a{ends[0]}; auto const &b{ends[partner]}; std::vector others; for (int i = 1; i < 4; ++i) if (i != partner) others.push_back(i); if (glm::dot(ends[0].inward, ends[others[0]].inward) < 0.0) std::swap(others[0], others[1]); auto const &c{ends[others[0]]}; auto const &d{ends[others[1]]}; auto const first{a.inward}; auto direction{c.inward}; auto const determinant{cross(first, direction)}; if (std::abs(determinant) < 1e-9) return false; auto const t{cross(plan_of(c.point) - plan_of(a.point), direction) / determinant}; glm::dvec2 const crossing{plan_of(a.point) + first * t}; auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; auto const oldhalf{glm::distance(plan_of(a.point), crossing)}; auto const blade{switch_design::secant_blade(Shape.rail, Shape.radius, Shape.ratio)}; auto curve{editor_track::slip_of(Shape.radius, Shape.middle, angle, 0.5 * std::abs(switches[0]->fTrackWidth) + switches[0]->rail_dimensions().head + 0.002, blade.first, blade.second)}; curve.tip = switch_design::cut_tip(Shape.rail, curve.radius); auto const full{curve.tangent()}; auto radius{curve.radius}; auto half{full}; auto const height{(a.point.y + b.point.y) * 0.5}; std::vector> states; std::vector created; std::vector removed; if (half > oldhalf) { for (auto const *e : {&a, &b, &c, &d}) { if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance)) { half = oldhalf; radius = curve.radius * half / full; state.status = format(STR_C("R limited to %d m, a neighbouring track isn't straight"), static_cast(radius)); break; } auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)}; auto const reach{std::abs(glm::dot(plan_of(line.end) - crossing, -e->inward))}; auto const reachstart{std::abs(glm::dot(plan_of(line.start) - crossing, -e->inward))}; if (std::max(reach, reachstart) - 0.5 < half) { half = std::min(half, std::max(reach, reachstart) - 0.5); radius = curve.radius * half / full; state.status = format(STR_C("R reduced to %d m to fit the straights"), static_cast(radius)); } } } for (auto *track : parts) editor_track::retire(*track); removed.insert(removed.end(), parts.begin(), parts.end()); TTrack *style{a.outer != nullptr && a.outer->eType == tt_Normal ? a.outer : switches[0]}; std::array points; std::array order{&a, &b, &c, &d}; for (int i = 0; i < 4; ++i) { auto const &e{*order[i]}; auto const outward{-e.inward}; auto const target{crossing + outward * half}; points[i] = {target.x, e.point.y, target.y}; if (half < oldhalf - 0.01) { auto *filler{editor_track::create_path(*style, [&] { segment_data path; path.points[segment_data::point::start] = points[i]; path.points[segment_data::point::end] = e.point; return path; }())}; created.push_back(filler); } else if (half > oldhalf + 0.01 && e.outer != nullptr) { auto const line{editor_track::find_straight(*e.outer, m_straights.tolerance)}; auto const alongold{glm::dot(plan_of(e.point) - plan_of(line.start), line.direction)}; auto const alongnew{glm::dot(target - plan_of(line.start), line.direction)}; cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr); } } auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, curve.scaled(half / full), *style)}; std::vector commitlist(parts2.begin(), parts2.end()); for (auto *track : created) if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end()) commitlist.push_back(track); editor_track::commit(commitlist); created.insert(created.end(), parts2.begin(), parts2.end()); push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); // the drives of the switches replaced go with them, the new ones get theirs std::vector drives; for (auto const *track : switches) if (auto const drive{find_switch_drive(*track)}) drives.push_back(drive->standing.instance); if (false == drives.empty() && remove_templates(drives) > 0) join_last_steps(); for (auto *track : parts2) if (track != nullptr && track->eType == tt_Switch && place_switch_drive(*track)) join_last_steps(); return true; } std::vector editor_mode::sample_chain(editor_track::chain const &Chain) { std::vector result; double station{0.0}; for (std::size_t i = 0; i < Chain.tracks.size(); ++i) { auto const &path{Chain.tracks[i]->m_paths.front()}; auto const forward{Chain.forward[i]}; bezier const curve{path}; auto const length{glm::distance(curve.p0, curve.p3)}; int const steps{std::max(8, static_cast(std::ceil(length / 0.5)))}; for (int k = (result.empty() ? 0 : 1); k <= steps; ++k) { auto const t{forward ? static_cast(k) / steps : 1.0 - static_cast(k) / steps}; auto const position{curve.point(t)}; auto const derivative{forward ? curve.first(t) : -curve.first(t)}; if (false == result.empty()) station += plan_distance(position, result.back().position); curve_sample sample; sample.station = station; sample.position = position; sample.tangent = glm::normalize(plan_of(derivative)); auto const roll{path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t}; sample.roll = static_cast(forward ? roll : -roll); sample.radius = path.radius; result.push_back(sample); } } return result; } editor_mode::curve_sample editor_mode::sample_at(std::vector const &Frame, double const Station) { if (Frame.empty()) return {}; if (Station <= Frame.front().station) return Frame.front(); if (Station >= Frame.back().station) return Frame.back(); auto const next{std::lower_bound(Frame.begin(), Frame.end(), Station, [](curve_sample const &Sample, double const Value) { return Sample.station < Value; })}; auto const previous{std::prev(next)}; auto const span{std::max(1e-9, next->station - previous->station)}; auto const f{(Station - previous->station) / span}; curve_sample result; result.station = Station; result.position = glm::mix(previous->position, next->position, f); result.tangent = glm::normalize(glm::mix(previous->tangent, next->tangent, f)); result.roll = static_cast(previous->roll + (next->roll - previous->roll) * f); result.radius = previous->radius; return result; } double editor_mode::nearest_station(std::vector const &Frame, glm::dvec3 const &Point) { double best{std::numeric_limits::max()}; double station{0.0}; for (std::size_t i = 0; i + 1 < Frame.size(); ++i) { glm::dvec2 const a{Frame[i].position.x, Frame[i].position.z}; glm::dvec2 const b{Frame[i + 1].position.x, Frame[i + 1].position.z}; auto const segment{b - a}; auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; auto const distance{glm::distance(plan_of(Point), a + segment * t)}; if (distance < best) { best = distance; station = Frame[i].station + (Frame[i + 1].station - Frame[i].station) * t; } } return station; } std::vector editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side) { auto const map = [&](glm::dvec3 const &Local, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) { auto const sample{sample_at(Frame, Station + Direction * Local.z)}; auto const along{sample.tangent * static_cast(Direction)}; glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast(Side)}; Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x); auto const planar{plan_of(sample.position) + across * Local.x}; auto const cant{glm::radians(static_cast(sample.roll) * Direction)}; auto const cone{-Local.x * Side * std::tan(cant)}; return glm::dvec3{planar.x, sample.position.y + Local.y + cone, planar.y}; }; auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) { auto const handle{plan_distance(Start, End) / 3.0}; segment_data path; path.points[segment_data::point::start] = Start; path.points[segment_data::point::end] = End; path.points[segment_data::point::control1] = {Startdirection.x * handle, (End.y - Start.y) / 3.0, Startdirection.y * handle}; path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle}; return path; }; auto const bend = [&](segment_data const &Local) { auto const &start{Local.points[segment_data::point::start]}; auto const &end{Local.points[segment_data::point::end]}; auto const &control1{Local.points[segment_data::point::control1]}; auto const &control2{Local.points[segment_data::point::control2]}; glm::dvec2 const chord{end.x - start.x, end.z - start.z}; auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; glm::dvec2 startdirection; glm::dvec2 enddirection; auto const p0{map(start, startdirection, localstart)}; auto const p3{map(end, enddirection, localend)}; auto path{hermite(p0, startdirection, p3, enddirection)}; { auto const cone = [&](double const T) { auto const point{bezier{path}.point(T)}; auto const sample{sample_at(Frame, nearest_station(Frame, point))}; auto const lateral{glm::dot(plan_of(point) - plan_of(sample.position), glm::dvec2{-sample.tangent.y, sample.tangent.x})}; return sample.position.y - lateral * std::tan(glm::radians(static_cast(sample.roll))); }; auto const h1{cone(1.0 / 3.0)}; auto const h2{cone(2.0 / 3.0)}; auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * p0.y + p3.y / 3.0}; auto const y2{3.0 * h2 - 1.5 * h1 + p0.y / 3.0 - 5.0 / 6.0 * p3.y}; path.points[segment_data::point::control1].y = y1 - p0.y; path.points[segment_data::point::control2].y = y2 - p3.y; } auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction}; auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction}; path.rolls = {static_cast(rollstart), static_cast(rollend)}; path.radius = Local.radius; { bezier const curve{path}; auto const begin{plan_of(curve.first(0.0))}; auto const finish{plan_of(curve.first(1.0))}; auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; if (Local.radius != 0.f && std::abs(turn) > 1e-6) path.radius = static_cast(std::copysign(curve.plan_length() / std::abs(turn), static_cast(Local.radius))); } return path; }; std::vector locals{Shape.local.begin(), Shape.local.end()}; if (Shape.twofold()) { locals.insert(locals.end(), Shape.second.begin(), Shape.second.end()); locals.push_back(Shape.link); } std::vector result; for (std::size_t i = 0; i < locals.size(); ++i) { auto const &local{locals[i]}; auto path{bend(local)}; for (auto const &piece : local.pieces) path.pieces.push_back(bend(piece)); // the main tracks follow the curve they are laid into if (i % 2 == 0) path.radius = sample_at(Frame, Station + Direction * 0.5 * (local.points[segment_data::point::start].z + local.points[segment_data::point::end].z)).radius; result.push_back(path); } return result; } void editor_mode::insert_curved_switch(int const Direction, int const Side) { auto &tool{m_switch}; auto const &shape{tool.templates[tool.armed]}; auto const total{tool.frame.back().station}; if (shape.length > total - 0.02) { tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length); return; } auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; auto const from{Direction > 0 ? station : station - shape.length}; auto const to{from + shape.length}; std::vector> states; std::vector created; std::vector removed; std::vector candidates{tool.frame_tracks}; for (auto const cut : {from, to}) { auto const point{sample_at(tool.frame, cut).position}; for (auto *candidate : std::vector(candidates)) { if (candidate->m_editorremoved || candidate->eType != tt_Normal) continue; auto const t{editor_track::nearest_parameter(*candidate, point)}; auto const found{editor_track::point_at(*candidate, t)}; if (plan_distance(found, point) > 0.05) continue; if (t > 0.002 && t < 0.998) { if (std::find(created.begin(), created.end(), candidate) == created.end() && std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == candidate; })) states.emplace_back(candidate, editor_track::capture(*candidate)); if (auto *second{editor_track::split_path(*candidate, t)}) { created.push_back(second); candidates.push_back(second); } } break; } } for (auto *candidate : candidates) { if (candidate->m_editorremoved) continue; auto const middle{editor_track::point_at(*candidate, 0.5)}; auto const where{nearest_station(tool.frame, middle)}; if (where > from + 0.01 && where < to - 0.01) { editor_track::retire(*candidate); removed.push_back(candidate); } } auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; auto const made{create_switch_set(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; created.insert(created.end(), made.begin(), made.end()); if (made.size() == 1) tool.status = format(STR_C("Switch %s inserted into the curve, Ctrl+Z takes it back"), made.front()->name().c_str()); auto const placed{created}; push_track_snapshot(std::move(states), std::move(created), std::move(removed)); straight_refresh(); switch_placed(placed); }