diff --git a/application/editormode.cpp b/application/editormode.cpp index 32f991c5..d89334e4 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -4994,6 +4994,8 @@ void editor_mode::ortho_pan_stop() void editor_mode::on_scroll(double const Xoffset, double const Yoffset) { + if (false == ImGui::GetIO().WantCaptureMouse && track_wheel(Yoffset != 0.0 ? Yoffset : Xoffset)) + return; if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) return; auto const before{Global.EditorOrthoExtent}; diff --git a/application/editormode.h b/application/editormode.h index c7dbd391..6944c986 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1006,7 +1006,28 @@ class editor_mode : public application_mode, private editor_track::observer // adds a straight (Side 0) or a curve to the left (1) or the right (-1) at the free end of the selected track void extend_add(int const Side); void render_extend_ui(); - bool m_extend_freehand{false}; // dragging the ring of a free end draws a curve through the cursor, not a straight + enum class track_layout + { + arc_straight, + straight_arc, + arc, + straight + }; + track_layout m_track_layout{track_layout::arc_straight}; + double m_join_radius{0.0}; + int m_join_variant{0}; + TTrack const *m_join_track{nullptr}; + editor_track::point_ref m_join_point; + mutable alignment::join_result m_join_last; + bool m_shift_wheel{false}; + bool track_wheel(double const Offset); + std::string track_shaping(bool const Joined) const; + void render_layout_combo(); + bool shift_straight() const; + double design_speed(TTrack const *From, TTrack const *To) const; + std::vector layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const; + std::vector join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const; double m_extend_length{25.0}; double m_extend_radius{300.0}; bool extend_snap(editor_track::snap_target &Target) const; diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp index 98d4a1e2..593ade8a 100644 --- a/application/editormodeselftest.cpp +++ b/application/editormodeselftest.cpp @@ -88,6 +88,9 @@ http://mozilla.org/MPL/2.0/. // terrainturn the textures of the palette entry turned // terrainwater [size] material and repeat of a body of water; terrainwaterdefault // orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given +// join [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other +// layto from the free end at the first point towards the second, as a drag in the layout +// joints ... the joints check over the scenery, the issues around the points logged // screenshot // save // log @@ -971,6 +974,99 @@ void editor_mode::selftest_step() else WriteLog("SELFTEST gauge " + name + ": no such track"); } + else if (command == "join" || command == "layto") + { + glm::dvec3 a{0.0}, b{0.0}; + words >> a.x >> a.z >> b.x >> b.z; + auto const from{editor_track::find_free_end(nullptr, 1, a, 3.0, {})}; + extend_tool tool; + if (from.track == nullptr || false == free_end(*from.track, from.point.path, from.point.kind == editor_track::point_kind::end, tool)) + { + WriteLog("SELFTEST " + command + ": no free end at the first point"); + continue; + } + std::vector pieces; + std::string error; + double length{0.0}; + if (command == "join") + { + double radius{0.0}; + int variant{0}, transitions{0}; + words >> radius >> variant >> transitions; + auto const to{editor_track::find_free_end(nullptr, 1, b, 3.0, {from.track})}; + if (to.track == nullptr) + { + WriteLog("SELFTEST join: no free end at the second point"); + continue; + } + m_join_radius = radius; + m_join_variant = variant; + m_lay.transitions = transitions != 0; + pieces = join_pieces(tool.track, from.point, tool.point, tool.direction, to, length, error); + } + else + { + int layout{0}; + words >> layout >> m_lay.radius; + m_track_layout = static_cast(std::clamp(layout, 0, 3)); + m_extend = tool; + m_extend.active = true; + m_extend.mouse = {b.x, tool.point.y, b.z}; + m_extend.snap = {}; + pieces = extend_pieces(); + m_extend.active = false; + } + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*tool.track, piece)); + editor_track::commit(created); + if (false == created.empty()) + push_track_snapshot({}, created); + char const *const kinds[]{"none", "straight", "arc", "curve", "reverse curve", "free curve"}; + auto const &join{m_join_last}; + WriteLog(format("SELFTEST %s: %s, %zu paths, R %.1f m, cant %.0f mm, transitions %.1f m, V %.0f km/h, variants %d%s", command.c_str(), kinds[static_cast(join.kind)], pieces.size(), join.radius, join.cant, + join.transition, join.speed, join.variants, error.empty() ? "" : (", " + error).c_str())); + auto heading{tool.direction}; + glm::dvec3 previous{tool.point}; + for (auto const *track : created) + { + auto const &path{track->m_paths.front()}; + geometry::bezier const curve{path}; + auto const begin{glm::normalize(geometry::plan_of(curve.first(0.0)))}; + auto const finish{glm::normalize(geometry::plan_of(curve.first(1.0)))}; + WriteLog(format("SELFTEST %s path %s: L %.2f m, R %.1f m, rolls %.2f %.2f, kink %.4f deg, step %.4f m", command.c_str(), track->name().c_str(), curve.plan_length(), static_cast(path.radius), + static_cast(path.rolls[0]), static_cast(path.rolls[1]), glm::degrees(std::abs(geometry::signed_angle(heading, begin))), glm::distance(previous, path.points[segment_data::point::start]))); + heading = finish; + previous = path.points[segment_data::point::end]; + } + if (false == created.empty()) + WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b))); + } + else if (command == "joints") + { + std::vector points; + for (glm::dvec3 point{0.0}; words >> point.x >> point.z;) + points.push_back(point); + auto const scope{m_joints.scope}; + m_joints.scope = 1; + joints_start(); + while (m_joints.next < m_joints.queue.size()) + joints_step(); + m_joints.scope = scope; + char const *const kinds[]{"gap", "step", "kink", "cant jump", "grade break", "free end"}; + int around{0}; + for (auto const &issue : m_joints.issues) + { + if (issue.type == joint_issue::kind::open) + continue; + if (std::none_of(points.begin(), points.end(), [&](glm::dvec3 const &Point) { return geometry::plan_distance(Point, issue.position) < 5.0; })) + continue; + ++around; + WriteLog(format("SELFTEST joints: %s %.3f at %.1f %.1f, %s / %s", kinds[static_cast(issue.type)], issue.value, issue.position.x, issue.position.z, issue.track != nullptr ? issue.track->name().c_str() : "-", + issue.other != nullptr ? issue.other->name().c_str() : "-")); + } + WriteLog(format("SELFTEST joints: %d issues around the points, %zu in all", around, m_joints.issues.size())); + } else if (command == "deletetrack") { delete_selected_track(); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 0b264b78..7c021098 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -34,6 +34,7 @@ http: #include #include #include +#include namespace { @@ -182,6 +183,37 @@ segment_data turned_around(segment_data const &Path) return result; } +double joint_roll(TTrack const &Track, editor_track::point_ref const &Point) +{ + if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) + return 0.0; + auto const &path{Track.m_paths[Point.path]}; + return Point.kind == editor_track::point_kind::start ? path.rolls[0] : -path.rolls[1]; +} + +std::vector straight_piece(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Length) +{ + if (Length < 0.5) + return {}; + segment_data path; + path.points[segment_data::point::start] = Point; + path.points[segment_data::point::end] = {Point.x + Direction.x * Length, Point.y + Grade * Length, Point.z + Direction.y * Length}; + return {path}; +} + +std::vector arc_through(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec2 const &Offset) +{ + auto const along{glm::dot(Offset, Direction)}; + auto const lateral{cross(Direction, Offset)}; + auto const chord{glm::length(Offset)}; + if (along <= 0.0 || chord < 0.5) + return {}; + if (std::abs(lateral) < 0.005 * chord + 0.05) + return straight_piece(Point, Direction, Grade, along); + auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; + return arc_pieces(Point, Direction, Grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1); +} + // curvature of the shape at Chainage, positive turning from x towards z double curvature_at(alignment::result const &Shape, double const Chainage) { @@ -3466,7 +3498,11 @@ std::vector editor_mode::track_key_hints(bool const All) hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "straight along it, to another free end: a curve joining them"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; else { - hints = {{"LMB", m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; + bool const chained{m_lay.points.size() == 1 && m_lay.start.track != nullptr}; + hints = {{"LMB", chained ? "lay the shape up to here and go on from its end" : m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Ctrl+wheel", "radius"}}; + if (chained || m_lay.mouse_snap.track != nullptr) + hints.push_back({"Shift+wheel", "arc / straight, at a free end: another shape"}); + hints.insert(hints.end(), {{"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}); if (glm::dvec2 heading; lay_heading(heading)) hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); } @@ -3477,10 +3513,8 @@ std::vector editor_mode::track_key_hints(bool const All) auto const step{std::min(m_straights.detour.size(), 3)}; hints = {{"Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}}; } - else if (m_extend.active && m_extend_freehand) - hints = {{"Drag", "curve through the cursor, along the end: straight"}, {"Ctrl", "straight"}, {"Release", "build"}, {"Esc", "cancel"}}; else if (m_extend.active) - hints = {{"Drag", "straight along the end, to a free end: a curve joining it"}, {"Shift", "straight, joins nothing"}, {"Release", "build"}, {"Esc", "cancel"}}; + hints = {{"Drag", "the shape chosen towards the cursor, to a free end: joins it"}, {"Ctrl+wheel", "radius"}, {"Shift+wheel", "arc / straight, at a free end: another shape"}, {"Shift", "straight, joins nothing"}, {"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"}}; else if (m_track_tab == track_tab::lay) @@ -3605,7 +3639,7 @@ std::string editor_mode::track_readout() const auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)}; if (snapped) text += "\njoins " + name(target.track); - return text; + return text + "\n" + track_shaping(snapped); } if (m_lay.active) { @@ -3634,6 +3668,10 @@ std::string editor_mode::track_readout() const text += "\njoins " + name(lay.mouse_snap.track); if (false == lay.preview_error.empty()) text += "\n" + lay.preview_error; + if (lay.points.size() == 1 && (lay.start.track != nullptr || lay.mouse_snap.track != nullptr)) + text += "\n" + track_shaping(lay.mouse_snap.track != nullptr); + else if (false == lay.points.empty()) + text += "\n" + format(STR_C("R %.0f m Ctrl+wheel: radius"), lay.radius); return text; } auto const &state{m_straights}; @@ -3697,6 +3735,32 @@ std::string editor_mode::track_readout() const return {}; } +std::string editor_mode::track_shaping(bool const Joined) const +{ + if (false == Joined) + { + char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; + auto text{std::string{STR_C(names[static_cast(m_track_layout)])}}; + if (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) + text += format(" R %.0f m", m_lay.radius); + return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: arc / straight"); + } + auto const &join{m_join_last}; + char const *const kinds[]{"", "straight", "arc", "curve", "reverse curve", "free curve"}; + std::string text{STR_C(kinds[static_cast(join.kind)])}; + if (join.radius > 0.0) + text += format(" R %.0f m", join.radius) + (m_join_radius > 0.0 ? std::string{} : std::string{" "} + STR("(auto)")); + if (join.cant > 0.0) + text += format(STR_C(" cant %.0f mm"), join.cant); + if (join.transition > 0.0) + text += format(STR_C(" transitions %.0f m"), join.transition); + if (join.speed > 0.0 && (join.cant > 0.0 || join.transition > 0.0)) + text += format(" V %.0f km/h", join.speed); + if (join.variants > 1) + text += format(" %d/%d", (m_join_variant % join.variants + join.variants) % join.variants + 1, join.variants); + return text + "\n" + STR("Ctrl+wheel: radius, Shift+wheel: another shape"); +} + void editor_mode::draw_track_hints() { ImGuiIO const &io = ImGui::GetIO(); @@ -3775,13 +3839,24 @@ void editor_mode::update_build_tools() auto const &io{ImGui::GetIO()}; glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; m_straights.detour_mouse = ground; + if (false == io.KeyShift) + m_shift_wheel = false; + auto const joining = [&](editor_track::snap_target const &Target) { + if (Target.track == m_join_track && Target.point == m_join_point) + return; + m_join_track = Target.track; + m_join_point = Target.point; + m_join_radius = 0.0; + m_join_variant = 0; + }; if (m_extend.active) { screen_projection const projection; glm::dvec3 level; m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast(kMaxPlacementDistance) ? level : ground; typed_extend(); - m_extend.snap = m_typed.empty() && false == io.KeyShift && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + m_extend.snap = m_typed.empty() && false == shift_straight() && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + joining(m_extend.snap); } if (m_switch.placing) m_switch.mouse = cursor_level(m_switch.point.y); @@ -3800,7 +3875,7 @@ void editor_mode::update_build_tools() else if (false == lay_projection.on_level(io.MousePos, height, mouse)) mouse = ground; glm::dvec2 heading; - bool const straight{io.KeyShift && lay_heading(heading)}; + bool const straight{shift_straight() && lay_heading(heading)}; if (straight) { auto const &last{lay.points.back()}; @@ -3809,6 +3884,8 @@ void editor_mode::update_build_tools() } lay.mouse = typed_lay(mouse); lay.mouse_snap = straight ? editor_track::snap_target{} : m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + if (false == m_extend.active) + joining(lay.points.empty() ? editor_track::snap_target{} : lay.mouse_snap); if (lay.points.empty()) return; auto points{lay.points}; @@ -4115,40 +4192,12 @@ std::vector editor_mode::extend_pieces() const editor_track::snap_target target; if (extend_snap(target)) { - alignment::design design; - design.norms = m_route.design.norms; - design.shape = m_route.design.shape; - design.arc_piece_angle = 90.0; - design.start = tool.point; - design.end = target.position; - design.start_direction = tool.direction; - design.end_direction = glm::normalize(plan_of(target.direction)); - design.start_grade = tool.grade; - design.end_grade = grade_of(target.direction); - auto const speed{editor_track::velocity(*tool.track)}; - design.speed = speed > 0.0 ? speed : 100.0; - if (false == alignment::collinear(design)) - { - alignment::vertex vertex; - vertex.radius = 100000.0; - auto const recommended{alignment::recommend(design.speed, 1000.0, design.norms)}; - vertex.transition_in = vertex.transition_out = recommended.transition; - glm::dvec2 intersection; - if (alignment::tangent_intersection(design, intersection)) - { - design.vertices.push_back(vertex); - } - else - { - vertex.offset = plan_distance(design.start, design.end) / 3.0; - design.vertices = {vertex, vertex}; - } - } - // a track which doesn't fit to the free end isn't built towards the cursor instead, it'd miss the end - auto const shape{alignment::compute(design)}; - return shape.valid ? alignment::pieces(shape, design, 0) : result; + double length; + std::string error; + return join_pieces(tool.track, {tool.path, tool.atend ? editor_track::point_kind::end : editor_track::point_kind::start}, tool.point, tool.direction, target, length, error); } - if (false == ImGui::GetIO().KeyShift && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) + m_join_last = {}; + if (false == shift_straight() && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) { auto const &diverging{tool.track->m_paths[1]}; bezier const curve{diverging}; @@ -4170,26 +4219,156 @@ std::vector editor_mode::extend_pieces() const return pieces; } } - glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + if (shift_straight()) + return straight_piece(tool.point, tool.direction, tool.grade, glm::dot(glm::dvec2{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}, tool.direction)); + auto radius{m_lay.radius}; + if (auto const values{typed_values()}; false == values.empty() && (m_track_layout == track_layout::arc_straight || m_track_layout == track_layout::straight_arc) && std::abs(values[0]) >= 1.0) + radius = std::abs(values[0]); + return layout_pieces(tool.point, tool.direction, tool.grade, tool.mouse, radius); +} + +std::vector editor_mode::layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const +{ + glm::dvec2 const offset{Mouse.x - Point.x, Mouse.z - Point.z}; + auto const along{glm::dot(offset, Direction)}; + auto const lateral{cross(Direction, offset)}; auto const chord{glm::length(offset)}; if (chord < 0.5) - return result; - auto const &direction{tool.direction}; - auto const along{glm::dot(offset, direction)}; - auto const lateral{direction.x * offset.y - direction.y * offset.x}; - auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; - if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift || std::abs(lateral) < 0.005 * chord + 0.05) + return {}; + if (m_track_layout == track_layout::straight) + return straight_piece(Point, Direction, Grade, along); + if (m_track_layout == track_layout::arc || std::abs(lateral) < 0.005 * chord + 0.05) + return arc_through(Point, Direction, Grade, offset); + int const side{lateral > 0.0 ? 1 : -1}; + glm::dvec2 const normal{-Direction.y * side, Direction.x * side}; + auto const radius{std::max(1.0, Radius)}; + if (radius >= chord * chord / (2.0 * std::abs(lateral))) + return arc_through(Point, Direction, Grade, offset); + if (m_track_layout == track_layout::arc_straight) { - if (along < 0.5) - return result; - segment_data path; - path.points[segment_data::point::start] = tool.point; - path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along}; - result.push_back(path); - return result; + auto const centre{plan_of(Point) + normal * radius}; + auto const towards{plan_of(Mouse) - centre}; + auto const distance{glm::length(towards)}; + auto const spread{std::acos(std::clamp(radius / distance, -1.0, 1.0))}; + auto const radial0{plan_of(Point) - centre}; + double rest{-1.0}; + double angle{0.0}; + for (auto const sign : {1.0, -1.0}) + { + auto const radial{turned(towards / distance, sign * spread) * radius}; + auto turn{side * signed_angle(radial0, radial)}; + if (turn < 0.0) + turn += 2.0 * glm::pi(); + auto const heading{turned(Direction, side * turn)}; + auto const remaining{glm::dot(plan_of(Mouse) - centre - radial, heading)}; + if (remaining > 0.0 && (rest < 0.0 || turn < angle)) + { + rest = remaining; + angle = turn; + } + } + if (rest < 0.0 || angle > glm::pi()) + return {}; + auto pieces{arc_pieces(Point, Direction, Grade, radius, angle, side)}; + auto const tail{straight_piece(pieces.back().points[segment_data::point::end], turned(Direction, side * angle), Grade, rest)}; + pieces.insert(pieces.end(), tail.begin(), tail.end()); + return pieces; } - auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; - return arc_pieces(tool.point, direction, tool.grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1); + auto const across{radius - std::abs(lateral)}; + if (std::abs(lateral) > 2.0 * radius) + return arc_through(Point, Direction, Grade, offset); + auto const lead{along - std::sqrt(std::max(0.0, radius * radius - across * across))}; + if (lead < 0.5) + return arc_through(Point, Direction, Grade, offset); + auto const angle{2.0 * std::atan2(std::abs(lateral), along - lead)}; + if (angle > glm::pi()) + return {}; + auto pieces{straight_piece(Point, Direction, Grade, lead)}; + auto const arc{arc_pieces(pieces.back().points[segment_data::point::end], Direction, Grade, radius, angle, side)}; + pieces.insert(pieces.end(), arc.begin(), arc.end()); + return pieces; +} + +double editor_mode::design_speed(TTrack const *From, TTrack const *To) const +{ + double speed{0.0}; + for (auto const *track : {From, To}) + { + if (track == nullptr) + continue; + auto const velocity{editor_track::velocity(*track)}; + if (velocity > 0.0) + speed = speed > 0.0 ? std::min(speed, velocity) : velocity; + } + if (speed <= 0.0) + speed = m_lay.style.velocity > 0.0 ? m_lay.style.velocity : 100.0; + return speed; +} + +std::vector editor_mode::join_pieces(TTrack const *From, editor_track::point_ref const &Frompoint, glm::dvec3 const &Start, glm::dvec2 const &Direction, editor_track::snap_target const &End, double &Length, + std::string &Error) const +{ + alignment::join_request request; + request.start.position = Start; + request.start.direction = Direction; + request.start.roll = From != nullptr ? -joint_roll(*From, Frompoint) : 0.0; + request.end.position = End.position; + request.end.direction = plan_of(End.direction); + request.end.roll = End.track != nullptr ? joint_roll(*End.track, End.point) : 0.0; + request.radius = m_join_radius; + request.variant = m_join_variant; + request.transitions = m_lay.transitions; + request.speed = design_speed(From, End.track); + request.kink = m_joints.angle * 0.5; + request.shape = m_route.design.shape; + request.norms = m_route.design.norms; + auto result{alignment::join(request)}; + if (m_lay.transitions && result.kind != alignment::join_kind::straight && result.kind != alignment::join_kind::arc && glm::length(request.end.direction) > 1e-9) + { + auto const grade{(End.position.y - Start.y) / std::max(1.0, plan_distance(Start, End.position))}; + auto const in{From != nullptr && From->eType == tt_Normal ? ease_out_of(*From, Start, grade, request.speed, request.norms) : easing{}}; + auto const out{End.track != nullptr && End.track->eType == tt_Normal ? ease_out_of(*End.track, End.position, -grade, request.speed, request.norms) : easing{}}; + if (false == in.pieces.empty() || false == out.pieces.empty()) + { + auto middle{request}; + if (false == in.pieces.empty()) + { + middle.start.position = in.end; + middle.start.direction = in.direction; + middle.start.roll = in.pieces.back().rolls[1]; + } + if (false == out.pieces.empty()) + { + middle.end.position = out.end; + middle.end.direction = -out.direction; + middle.end.roll = -out.pieces.back().rolls[1]; + } + auto const gap{plan_of(middle.end.position) - plan_of(middle.start.position)}; + if (glm::length(gap) > 5.0 && glm::dot(glm::normalize(middle.start.direction), gap) > 0.0 && glm::dot(glm::normalize(middle.end.direction), gap) > 0.0) + { + auto eased{alignment::join(middle)}; + if (eased.kind != alignment::join_kind::none && eased.kind != alignment::join_kind::bezier) + { + std::vector pieces{in.pieces}; + pieces.insert(pieces.end(), eased.pieces.begin(), eased.pieces.end()); + for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece) + pieces.push_back(turned_around(*piece)); + eased.pieces = std::move(pieces); + eased.length += (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length); + result = std::move(eased); + } + } + } + } + Length = result.length; + Error = result.kind == alignment::join_kind::bezier ? std::string{STR_C("No curve fits between the ends, they are joined with a free curve")} : std::string{}; + m_join_last = result; + return result.pieces; +} + +bool editor_mode::shift_straight() const +{ + return ImGui::GetIO().KeyShift && false == m_shift_wheel; } void editor_mode::finish_extend() @@ -4299,8 +4478,70 @@ void editor_mode::render_extend_ui() ImGui::SameLine(); if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) extend_add(-1); - ImGui::Checkbox(STR_C("Freehand"), &m_extend_freehand); - item_tooltip("Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end"); + ImGui::PushID("extend"); + render_layout_combo(); + ImGui::PopID(); +} + +void editor_mode::render_layout_combo() +{ + char const *const names[]{"Arc, then straight", "Straight, then arc", "Arc through the cursor", "Straight"}; + auto current{static_cast(m_track_layout)}; + ImGui::PushItemWidth(200.0f); + if (ImGui::BeginCombo(STR_C("Shape from a free end"), STR_C(names[current]))) + { + for (int i = 0; i < 4; ++i) + if (ImGui::Selectable(STR_C(names[i]), i == current)) + m_track_layout = static_cast(i); + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + item_tooltip("What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius"); +} + +bool editor_mode::track_wheel(double const Offset) +{ + auto const &io{ImGui::GetIO()}; + if ((false == io.KeyCtrl && false == io.KeyShift) || Offset == 0.0) + return false; + bool const laying{m_lay.active && false == m_lay.points.empty()}; + if (false == m_extend.active && false == laying) + return false; + int const step{Offset > 0.0 ? 1 : -1}; + editor_track::snap_target target; + bool const joined{m_extend.active ? extend_snap(target) : m_lay.mouse_snap.track != nullptr}; + static double const radii[]{100.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, 4000.0, 6000.0}; + auto const next_radius = [&](double const Current) { + if (step > 0) + { + for (auto const radius : radii) + if (radius > Current * 1.001) + return radius; + return std::end(radii)[-1]; + } + for (auto radius{std::rbegin(radii)}; radius != std::rend(radii); ++radius) + if (*radius < Current * 0.999) + return *radius; + return radii[0]; + }; + if (io.KeyCtrl) + { + if (false == joined) + m_lay.radius = next_radius(m_lay.radius); + else if (m_join_radius <= 0.0) + m_join_radius = next_radius(m_join_last.radius > 0.0 ? m_join_last.radius : 500.0); + else if (step > 0 && m_join_radius >= std::end(radii)[-1]) + m_join_radius = 0.0; + else + m_join_radius = next_radius(m_join_radius); + return true; + } + m_shift_wheel = true; + if (joined) + m_join_variant += step; + else + m_track_layout = static_cast((static_cast(m_track_layout) + 4 - step) % 4); + return true; } std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const @@ -4309,6 +4550,24 @@ std::vector editor_mode::lay_pieces(std::vector const Length = 0.0; if (Points.size() < 2) return {}; + if (Points.size() == 2 && (End.track != nullptr || tool.start.track != nullptr)) + { + auto const leaving{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + if (glm::length(leaving) < 1e-6) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + if (End.track != nullptr) + return join_pieces(tool.start.track, tool.start.point, Points[0], glm::normalize(leaving), End, Length, Error); + m_join_last = {}; + auto const pieces{layout_pieces(Points[0], glm::normalize(leaving), -grade_of(tool.start.direction), Points[1], tool.radius)}; + for (auto const &piece : pieces) + Length += bezier{piece}.plan_length(); + if (pieces.empty()) + Error = STR_C("The track can't be laid through these points"); + return pieces; + } auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) @@ -4338,9 +4597,15 @@ std::vector editor_mode::lay_pieces(std::vector const result.pieces.clear(); return result; }; - auto const in{eased(tool.start, 0.0, 1.0)}; - auto const out{eased(End, shape.length, -1.0)}; - if (false == in.pieces.empty() || false == out.pieces.empty()) + auto const in{tool.transitions ? eased(tool.start, 0.0, 1.0) : easing{}}; + auto const out{tool.transitions ? eased(End, shape.length, -1.0) : easing{}}; + auto const room = [&]() { + auto const from{in.pieces.empty() ? Points.front() : in.end}; + auto const to{out.pieces.empty() ? Points.back() : out.end}; + auto const gap{plan_of(to) - plan_of(from)}; + return glm::length(gap) > 5.0 && glm::dot(in.pieces.empty() ? design.start_direction : in.direction, gap) > 0.0 && glm::dot(out.pieces.empty() ? design.end_direction : -out.direction, gap) > 0.0; + }; + if ((false == in.pieces.empty() || false == out.pieces.empty()) && room()) { auto points{Points}; auto starting{design.start_direction}; @@ -4380,7 +4645,7 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, design = {}; design.norms = m_route.design.norms; design.shape = m_route.design.shape; - design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0; + design.speed = design_speed(tool.start.track, nullptr); design.start = Points.front(); design.end = Points.back(); design.start_direction = Startdirection; @@ -4388,12 +4653,6 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, alignment::vertex corner; corner.radius = std::max(1.0, tool.radius); - if (tool.transitions) - { - auto const recommended{alignment::recommend(design.speed, corner.radius, design.norms)}; - corner.cant = recommended.cant; - corner.transition_in = corner.transition_out = recommended.transition; - } if (Points.size() == 2) { if (false == alignment::collinear(design)) @@ -4423,7 +4682,7 @@ alignment::result editor_mode::lay_shape(std::vector const &Points, design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); } } - return alignment::compute(design); + return alignment::compute_recommended(design, tool.transitions); } void editor_mode::lay_click() @@ -4447,8 +4706,31 @@ void editor_mode::lay_click() auto const point{placement_on_ground(tool.mouse)}; if (plan_distance(point, tool.points.back()) < 1.0) return; - tool.points.push_back(point); m_typed.clear(); + if (tool.points.size() == 1 && tool.start.track != nullptr) + { + tool.points.push_back(tool.mouse); + lay_finish({}); + if (false == tool.points.empty()) + { + tool.points.pop_back(); + return; + } + auto *last{dynamic_cast(m_node)}; + if (last == nullptr || last->m_paths.empty()) + return; + auto const &path{last->m_paths.front()}; + editor_track::snap_target next; + next.track = last; + next.point = {0, editor_track::point_kind::end}; + next.position = path.points[segment_data::point::end]; + auto const &control{path.points[segment_data::point::control2]}; + next.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[segment_data::point::start] - next.position); + tool.start = next; + tool.points = {next.position}; + return; + } + tool.points.push_back(point); } void editor_mode::lay_finish(editor_track::snap_target const &End) @@ -4569,8 +4851,9 @@ void editor_mode::render_lay_ui() tool.radius = std::max(10.0, tool.radius); ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); - item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant"); + item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows"); ImGui::PopItemWidth(); + render_layout_combo(); if (false == ImGui::TreeNode(STR_C("More settings"))) { render_lay_status(); diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp index 9e19d1b2..158172e4 100644 --- a/application/editormodetrackux.cpp +++ b/application/editormodetrackux.cpp @@ -731,7 +731,7 @@ char const *editor_mode::typed_meaning() const { auto const &straights{m_straights}; if (m_extend.active) - return m_extend_freehand && false == ImGui::GetIO().KeyCtrl ? "type the radius, a space, the length" : "type the length"; + return m_track_layout == track_layout::straight || shift_straight() ? "type the length" : m_track_layout == track_layout::arc ? "type the radius, a space, the length" : "type the radius"; if (straights.dragging) return straights.handle == 2 ? "type the shift, + to the right" : "type the length"; if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging)) @@ -800,7 +800,9 @@ void editor_mode::typed_extend() return; auto &tool{m_extend}; auto const &direction{tool.direction}; - if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl) + if (m_track_layout != track_layout::straight && m_track_layout != track_layout::arc && false == shift_straight()) + return; + if (m_track_layout == track_layout::straight || shift_straight()) { auto const length{std::max(0.0, values[0])}; tool.mouse = {tool.point.x + direction.x * length, tool.mouse.y, tool.point.z + direction.y * length}; @@ -817,7 +819,7 @@ void editor_mode::typed_extend() auto angle{values.size() > 1 ? std::abs(values[1]) / radius : side * geometry::signed_angle(radial, glm::dvec2{tool.mouse.x, tool.mouse.z} - centre)}; if (angle < 0.0) angle += 2.0 * glm::pi(); - angle = std::clamp(angle, 1e-3, 2.0 * glm::pi() - 0.1); + angle = std::clamp(angle, 1e-3, glm::pi()); auto const end{centre + geometry::turned(radial, side * angle)}; tool.mouse = {end.x, tool.mouse.y, end.y}; } diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp index cf894d63..cc32a0ce 100644 --- a/editor/editorAlignment.cpp +++ b/editor/editorAlignment.cpp @@ -17,6 +17,7 @@ http: #include #include #include +#include namespace alignment { @@ -378,6 +379,7 @@ result fit_between(design const &Design) double tangent_in{0.0}; double tangent_out{0.0}; double deflection{0.0}; + double cant{0.0}; bool compound{false}; std::vector arcs; std::function tangents; @@ -518,6 +520,44 @@ result fit_between(design const &Design) } r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); } + for (std::size_t k = 0; k < count; ++k) + { + auto &fit{fits[k]}; + auto const &vertex{Design.vertices[k]}; + fit.cant = std::max(0.0, vertex.cant); + if (fit.compound || fit.deflection < 1e-7 || fit.transition_in + fit.transition_out <= 0.0) + continue; + auto const angles = [&](double const Factor) { + return transition_point(Design.shape, fit.transition_in * Factor, fit.radius, 1.0).angle + transition_point(Design.shape, fit.transition_out * Factor, fit.radius, 1.0).angle; + }; + auto const minimum{std::min(fit.radius * fit.deflection, std::max(Design.norms.arc_min, Design.norms.arc_min_factor * Design.speed))}; + if (fit.radius * (fit.deflection - angles(1.0)) < minimum - 1e-6) + { + auto const target{fit.deflection - minimum / fit.radius}; + double low{0.0}, high{1.0}; + for (int i = 0; i < 50; ++i) + { + auto const middle{(low + high) * 0.5}; + (angles(middle) > target ? high : low) = middle; + } + fit.transition_in *= low; + fit.transition_out *= low; + r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to %.1f / %.1f m, the circular arc keeps %.0f m", k + 1, fit.transition_in, fit.transition_out, minimum)); + } + if (fit.transition_in < 1.0) + fit.transition_in = 0.0; + if (fit.transition_out < 1.0) + fit.transition_out = 0.0; + auto const shortest{std::min(fit.transition_in, fit.transition_out)}; + if (fit.cant <= 0.0 || (fit.transition_in >= vertex.transition_in - 1e-6 && fit.transition_out >= vertex.transition_out - 1e-6)) + continue; + auto const speed{std::max(1.0, Design.speed)}; + auto const ramp{std::min(shortest * 1000.0 / (Design.norms.ramp_factor * speed), Design.norms.cant_rate * 3.6 * shortest / speed)}; + if (ramp >= fit.cant) + continue; + fit.cant = ramp < 5.0 ? 0.0 : std::floor(ramp / 5.0) * 5.0; + r.warnings.emplace_back(format("Vertex %zu: cant lowered to %.0f mm, the shortened transition curves can't ramp more", k + 1, fit.cant)); + } std::vector vertexchainage(count, 0.0); glm::dvec2 cursor{start}; @@ -650,7 +690,7 @@ result fit_between(design const &Design) curve.vertex = static_cast(k); curve.radius = radius; curve.turn = side; - curve.cant = std::max(0.0, vertex.cant); + curve.cant = fit.cant; if (lengthin > 0.0) { curve.kind = element_kind::transition_in; @@ -804,6 +844,227 @@ result compute(design const &Design) return r; } +double speed_limit(double const Radius, limits const &Limits) +{ + if (Radius <= 0.0) + return 0.0; + auto const factor{Limits.gauge / (kGravity * 12.96)}; + auto const allowance{Limits.gauge * Limits.unbalanced / kGravity}; + return std::sqrt(Radius * (Limits.cant_max + allowance) / factor); +} + +result compute_recommended(design &Design, bool const Transitions) +{ + for (auto &vertex : Design.vertices) + { + vertex.cant = 0.0; + vertex.transition_in = vertex.transition_out = 0.0; + } + auto r{compute(Design)}; + if (false == Transitions || false == r.valid) + return r; + auto const speed{Design.speed}; + for (int i = 0; i < 3; ++i) + { + bool accepted{false}; + for (auto const share : {1.0, 0.75, 0.5, 0.35, 0.25}) + { + auto attempt{Design}; + auto slowest{speed}; + for (auto const &curve : r.curves) + { + if (curve.vertex < 0 || curve.vertex >= static_cast(attempt.vertices.size())) + continue; + auto const allowed{std::max(1.0, std::min(speed, speed_limit(curve.radius, attempt.norms)))}; + auto const recommended{recommend(allowed, curve.radius, attempt.norms)}; + auto const length{recommended.transition * share}; + auto &vertex{attempt.vertices[curve.vertex]}; + vertex.transition_in = vertex.transition_out = length; + vertex.cant = std::min({recommended.cant, length * 1000.0 / (attempt.norms.ramp_factor * allowed), attempt.norms.cant_rate * 3.6 * length / allowed}); + vertex.cant = vertex.cant < 5.0 ? 0.0 : std::floor(vertex.cant / 5.0) * 5.0; + slowest = std::min(slowest, allowed); + } + attempt.speed = slowest; + auto next{compute(attempt)}; + if (false == next.valid) + continue; + auto const kept{std::all_of(next.curves.begin(), next.curves.end(), [&](curve_report const &Curve) { + auto const &vertex{attempt.vertices[Curve.vertex]}; + return Curve.transition_in >= 0.95 * vertex.transition_in && Curve.transition_out >= 0.95 * vertex.transition_out; + })}; + if (false == kept && share > 0.25) + continue; + Design = attempt; + r = next; + accepted = true; + break; + } + if (false == accepted) + break; + } + return r; +} + +namespace +{ + +double turning(result const &Result) +{ + double total{0.0}; + for (auto const &e : Result.elements) + { + if (e.radius <= 0.0) + continue; + if (e.kind == element_kind::arc) + total += e.length / e.radius; + else if (e.kind == element_kind::transition_in || e.kind == element_kind::transition_out) + total += e.length / (2.0 * e.radius); + } + return total; +} + +void spread_rolls(std::vector &Pieces, double const Start, double const End) +{ + auto const count{static_cast(Pieces.size())}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + Pieces[i].rolls = {static_cast(Start + (End - Start) * i / count), static_cast(Start + (End - Start) * (i + 1) / count)}; +} + +} // namespace + +join_result join(join_request const &Request) +{ + join_result chosen; + auto const &a{Request.start}; + auto const &b{Request.end}; + auto const chord{plan_of(b.position) - plan_of(a.position)}; + auto const distance{glm::length(chord)}; + if (distance < 0.05 || glm::length(a.direction) < 1e-9 || glm::length(b.direction) < 1e-9) + return chosen; + auto const startdirection{glm::normalize(a.direction)}; + auto const enddirection{glm::normalize(b.direction)}; + auto const tolerance{glm::radians(Request.kink)}; + auto const heading{std::abs(geometry::signed_angle(startdirection, enddirection))}; + auto const rise{b.position.y - a.position.y}; + std::vector candidates; + + if (std::abs(geometry::signed_angle(startdirection, chord)) <= tolerance && std::abs(geometry::signed_angle(chord, enddirection)) <= tolerance) + { + join_result straight; + straight.kind = join_kind::straight; + segment_data path; + path.points[segment_data::point::start] = a.position; + path.points[segment_data::point::end] = b.position; + straight.pieces = {path}; + spread_rolls(straight.pieces, a.roll, b.roll); + straight.length = glm::distance(a.position, b.position); + candidates.push_back(straight); + } + + auto const lateral{cross(startdirection, chord)}; + auto const along{glm::dot(startdirection, chord)}; + if (std::abs(lateral) > 1e-6) + { + auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; + int const side{lateral > 0.0 ? 1 : -1}; + auto const radius{distance * distance / (2.0 * std::abs(lateral))}; + if (angle < kPi - 1e-3 && std::abs(geometry::signed_angle(turned(startdirection, side * angle), enddirection)) <= tolerance) + { + join_result arc; + arc.kind = join_kind::arc; + arc.pieces = geometry::arc_pieces(a.position, startdirection, rise / (radius * angle), radius, angle, side); + arc.pieces.back().points[segment_data::point::end] = b.position; + spread_rolls(arc.pieces, a.roll, b.roll); + arc.radius = radius; + arc.length = radius * angle; + candidates.push_back(arc); + } + } + + auto const fitted = [&](join_kind const Kind) { + join_result curve; + design shape; + shape.norms = Request.norms; + shape.shape = Request.shape; + shape.speed = Request.speed; + shape.arc_piece_angle = 90.0; + shape.start = a.position; + shape.end = b.position; + shape.start_direction = startdirection; + shape.end_direction = enddirection; + shape.start_grade = shape.end_grade = rise / distance; + vertex corner; + corner.radius = Request.radius > 0.0 ? Request.radius : 100000.0; + if (Kind == join_kind::curve) + shape.vertices = {corner}; + else + { + corner.offset = distance / 3.0; + shape.vertices = {corner, corner}; + } + auto const r{compute_recommended(shape, Request.transitions)}; + auto const sharp{std::any_of(r.curves.begin(), r.curves.end(), [](curve_report const &Report) { return Report.radius < 10.0; })}; + if (false == r.valid || sharp || turning(r) > heading + kPi + 1e-6 || r.length > 3.0 * distance + 1.0) + return curve; + curve.kind = Kind; + curve.pieces = pieces(r, shape, 0); + if (curve.pieces.empty()) + return curve; + curve.pieces.front().rolls[0] = static_cast(a.roll); + curve.pieces.back().rolls[1] = static_cast(b.roll); + curve.length = r.length; + curve.speed = shape.speed; + curve.warnings = r.warnings; + curve.radius = std::numeric_limits::max(); + for (auto const &report : r.curves) + { + curve.radius = std::min(curve.radius, report.radius); + curve.transition = std::max({curve.transition, report.transition_in, report.transition_out}); + } + for (auto const &e : r.elements) + curve.cant = std::max(curve.cant, e.cant); + if (r.curves.empty()) + curve.radius = 0.0; + return curve; + }; + design probe; + probe.start = a.position; + probe.end = b.position; + probe.start_direction = startdirection; + probe.end_direction = enddirection; + glm::dvec2 intersection; + if (tangent_intersection(probe, intersection)) + { + if (auto curve{fitted(join_kind::curve)}; curve.kind != join_kind::none) + candidates.push_back(curve); + } + else if (auto curve{fitted(join_kind::reverse_curve)}; curve.kind != join_kind::none) + candidates.push_back(curve); + + if (candidates.empty()) + { + join_result fallback; + fallback.kind = join_kind::bezier; + auto const grade{rise / distance}; + auto const handle{distance / 3.0}; + segment_data path; + path.points[segment_data::point::start] = a.position; + path.points[segment_data::point::end] = b.position; + path.points[segment_data::point::control1] = glm::dvec3{startdirection.x, grade, startdirection.y} * handle; + path.points[segment_data::point::control2] = -glm::dvec3{enddirection.x, grade, enddirection.y} * handle; + fallback.pieces = {path}; + spread_rolls(fallback.pieces, a.roll, b.roll); + fallback.length = distance; + candidates.push_back(fallback); + } + auto const count{static_cast(candidates.size())}; + chosen = candidates[((Request.variant % count) + count) % count]; + chosen.variants = count; + if (chosen.speed <= 0.0) + chosen.speed = Request.speed; + return chosen; +} + bool tangent_intersection(design const &Design, glm::dvec2 &Point) { auto const start{plan_of(Design.start)}; diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp index 732bd454..914e7a7a 100644 --- a/editor/editorAlignment.hpp +++ b/editor/editorAlignment.hpp @@ -35,6 +35,8 @@ struct limits double vertical_factor{0.4}; double vertical_min{2000.0}; double tangent_min_time{0.0}; + double arc_min{20.0}; + double arc_min_factor{0.4}; }; struct compound_arc @@ -164,6 +166,8 @@ struct result }; result compute(design const &Design); +double speed_limit(double const Radius, limits const &Limits); +result compute_recommended(design &Design, bool const Transitions); bool tangent_intersection(design const &Design, glm::dvec2 &Point); bool collinear(design const &Design); @@ -184,4 +188,49 @@ std::size_t minimum_pieces(result const &Result, design const &Design); std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks = {}, std::vector const &Counts = {}, std::vector *Intervals = nullptr); +struct join_end +{ + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double roll{0.0}; +}; + +enum class join_kind +{ + none, + straight, + arc, + curve, + reverse_curve, + bezier +}; + +struct join_request +{ + join_end start; + join_end end; + double radius{0.0}; + int variant{0}; + bool transitions{false}; + double speed{100.0}; + double kink{0.1}; + transition_shape shape{transition_shape::cubic_parabola}; + limits norms; +}; + +struct join_result +{ + join_kind kind{join_kind::none}; + std::vector pieces; + double length{0.0}; + double radius{0.0}; + double cant{0.0}; + double transition{0.0}; + double speed{0.0}; + int variants{0}; + std::vector warnings; +}; + +join_result join(join_request const &Request); + } diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 58b30109..54e4f947 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -5213,3 +5213,73 @@ msgstr "Punkt znikania" msgid "Pedestrian crossing" msgstr "Przejście dla pieszych" + +# scenery editor: joining free ends, the shape from a free end on the wheel +msgid "No curve fits between the ends, they are joined with a free curve" +msgstr "Między końcami nie mieści się żaden łuk, połączono je krzywą swobodną" + +msgid "Arc, then straight" +msgstr "Łuk, potem prosta" + +msgid "Straight, then arc" +msgstr "Prosta, potem łuk" + +msgid "Arc through the cursor" +msgstr "Łuk przez kursor" + +msgid "Shape from a free end" +msgstr "Kształt z wolnego końca" + +msgid "What a drag or a click from a free end lays towards the cursor; Shift+wheel changes it, Ctrl+wheel the radius" +msgstr "Co przeciągnięcie lub kliknięcie z wolnego końca kładzie w stronę kursora; Shift+kółko zmienia kształt, Ctrl+kółko promień" + +msgid "Speed limit of the new paths, and the design speed of the transition curves and of the cant; the speed of the track joined comes first, and none higher than the radius allows" +msgstr "Prędkość nowych torów i prędkość projektowa krzywych przejściowych oraz przechyłki; pierwszeństwo ma prędkość toru, do którego się dołącza, i nigdy nie wyższa, niż pozwala promień" + +msgid "R %.0f m Ctrl+wheel: radius" +msgstr "R %.0f m Ctrl+kółko: promień" + +msgid "Ctrl+wheel: radius, Shift+wheel: arc / straight" +msgstr "Ctrl+kółko: promień, Shift+kółko: łuk / prosta" + +msgid "Ctrl+wheel: radius, Shift+wheel: another shape" +msgstr "Ctrl+kółko: promień, Shift+kółko: inny kształt" + +msgid "arc" +msgstr "łuk" + +msgid "curve" +msgstr "łuk z prostymi" + +msgid "reverse curve" +msgstr "łuk odwrotny (S)" + +msgid "free curve" +msgstr "krzywa swobodna" + +msgid "(auto)" +msgstr "(auto)" + +msgid " cant %.0f mm" +msgstr " przechyłka %.0f mm" + +msgid " transitions %.0f m" +msgstr " krzywe przejściowe %.0f m" + +msgid "the shape chosen towards the cursor, to a free end: joins it" +msgstr "wybrany kształt w stronę kursora, na wolny koniec: łączy z nim" + +msgid "Ctrl+wheel" +msgstr "Ctrl+kółko" + +msgid "Shift+wheel" +msgstr "Shift+kółko" + +msgid "arc / straight, at a free end: another shape" +msgstr "łuk / prosta, na wolnym końcu: inny kształt" + +msgid "lay the shape up to here and go on from its end" +msgstr "kładzie kształt dotąd i idzie dalej od jego końca" + +msgid "type the radius" +msgstr "wpisz promień"