17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 01:54:06 +01:00

feat(editor): two free ends joined without loops or forced curves, the shape from a free end on the wheel

- the join of two free ends (a drag from one onto the other, or a click of the lay tool on it) tries in turn: a straight when the joints check can't see the kinks (half its angle), one circular arc through both ends (a piece cut out of a curve comes back as it was, its cant kept), a curve with straights where the tangents meet, a reverse curve, and only then a free Bezier curve; a curve turning more than it needs to, sharper than 10 m or longer than three chords is never taken
- the transition curves eased out of a curved neighbour only when there's room for them between the ends, never crossing each other into a loop
- cant and transition curves recommended for the radius the curve really gets, at the speed of the joined track or of the tool and never above what the radius allows; the circular arc keeps at least max(20 m, 0.4 V) between the transitions, the cant lowered to what the shortened transitions can ramp, transitions under 1 m left out
- shapes from a free end: arc then straight, straight then arc, arc through the cursor, straight; Ctrl+wheel steps the radius, Shift+wheel the shape (at a free end: the other joins), the plain wheel still zooms; a click of the lay tool from a free end lays the shape and goes on from its end
- self-test commands join, layto and joints; Polish translation
This commit is contained in:
maj00r
2026-10-11 11:11:21 +02:00
parent 3ed864812f
commit 16c1ac4c7b
8 changed files with 860 additions and 76 deletions

View File

@@ -4994,6 +4994,8 @@ void editor_mode::ortho_pan_stop()
void editor_mode::on_scroll(double const Xoffset, double const Yoffset) 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) if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse)
return; return;
auto const before{Global.EditorOrthoExtent}; auto const before{Global.EditorOrthoExtent};

View File

@@ -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 // 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 extend_add(int const Side);
void render_extend_ui(); 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<segment_data> layout_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, glm::dvec3 const &Mouse, double const Radius) const;
std::vector<segment_data> 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_length{25.0};
double m_extend_radius{300.0}; double m_extend_radius{300.0};
bool extend_snap(editor_track::snap_target &Target) const; bool extend_snap(editor_track::snap_target &Target) const;

View File

@@ -88,6 +88,9 @@ http://mozilla.org/MPL/2.0/.
// terrainturn <palette entry> <degrees> the textures of the palette entry turned // terrainturn <palette entry> <degrees> the textures of the palette entry turned
// terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material> // terrainwater <index> <material> [size] material and repeat of a body of water; terrainwaterdefault <material>
// orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given // orthophoto <north> <east> <radius> <fit 0|1> [in scene 0|1] the imagery layer on, placed and fitted as given
// join <x> <z> <x> <z> [radius] [variant] [transitions 0|1] the free ends at the points joined, as a drag from one to the other
// layto <x> <z> <x> <z> <layout 0..3> <radius> from the free end at the first point towards the second, as a drag in the layout
// joints <x> <z>... the joints check over the scenery, the issues around the points logged
// screenshot // screenshot
// save // save
// log <text> // log <text>
@@ -971,6 +974,99 @@ void editor_mode::selftest_step()
else else
WriteLog("SELFTEST gauge " + name + ": no such track"); 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<segment_data> 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<track_layout>(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<TTrack *> 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<int>(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<double>(path.radius),
static_cast<double>(path.rolls[0]), static_cast<double>(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<glm::dvec3> 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<int>(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") else if (command == "deletetrack")
{ {
delete_selected_track(); delete_selected_track();

View File

@@ -34,6 +34,7 @@ http:
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <functional> #include <functional>
#include <iterator>
namespace namespace
{ {
@@ -182,6 +183,37 @@ segment_data turned_around(segment_data const &Path)
return result; return result;
} }
double joint_roll(TTrack const &Track, editor_track::point_ref const &Point)
{
if (Point.path < 0 || Point.path >= static_cast<int>(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<segment_data> 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<segment_data> 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 // curvature of the shape at Chainage, positive turning from x towards z
double curvature_at(alignment::result const &Shape, double const Chainage) double curvature_at(alignment::result const &Shape, double const Chainage)
{ {
@@ -3466,7 +3498,11 @@ std::vector<editor_mode::key_hint> 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"}}; 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 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)) if (glm::dvec2 heading; lay_heading(heading))
hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"});
} }
@@ -3477,10 +3513,8 @@ std::vector<editor_mode::key_hint> editor_mode::track_key_hints(bool const All)
auto const step{std::min<std::size_t>(m_straights.detour.size(), 3)}; auto const step{std::min<std::size_t>(m_straights.detour.size(), 3)};
hints = {{"Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}}; 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) 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) else if (m_switch.placing)
hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}};
else if (m_track_tab == track_tab::lay) 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)}; auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)};
if (snapped) if (snapped)
text += "\njoins " + name(target.track); text += "\njoins " + name(target.track);
return text; return text + "\n" + track_shaping(snapped);
} }
if (m_lay.active) if (m_lay.active)
{ {
@@ -3634,6 +3668,10 @@ std::string editor_mode::track_readout() const
text += "\njoins " + name(lay.mouse_snap.track); text += "\njoins " + name(lay.mouse_snap.track);
if (false == lay.preview_error.empty()) if (false == lay.preview_error.empty())
text += "\n" + lay.preview_error; 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; return text;
} }
auto const &state{m_straights}; auto const &state{m_straights};
@@ -3697,6 +3735,32 @@ std::string editor_mode::track_readout() const
return {}; 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<int>(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<int>(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() void editor_mode::draw_track_hints()
{ {
ImGuiIO const &io = ImGui::GetIO(); ImGuiIO const &io = ImGui::GetIO();
@@ -3775,13 +3839,24 @@ void editor_mode::update_build_tools()
auto const &io{ImGui::GetIO()}; auto const &io{ImGui::GetIO()};
glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; glm::vec2 const cursor{io.MousePos.x, io.MousePos.y};
m_straights.detour_mouse = ground; 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) if (m_extend.active)
{ {
screen_projection const projection; screen_projection const projection;
glm::dvec3 level; glm::dvec3 level;
m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast<double>(kMaxPlacementDistance) ? level : ground; m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast<double>(kMaxPlacementDistance) ? level : ground;
typed_extend(); 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) if (m_switch.placing)
m_switch.mouse = cursor_level(m_switch.point.y); 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)) else if (false == lay_projection.on_level(io.MousePos, height, mouse))
mouse = ground; mouse = ground;
glm::dvec2 heading; glm::dvec2 heading;
bool const straight{io.KeyShift && lay_heading(heading)}; bool const straight{shift_straight() && lay_heading(heading)};
if (straight) if (straight)
{ {
auto const &last{lay.points.back()}; auto const &last{lay.points.back()};
@@ -3809,6 +3884,8 @@ void editor_mode::update_build_tools()
} }
lay.mouse = typed_lay(mouse); 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, {}); 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()) if (lay.points.empty())
return; return;
auto points{lay.points}; auto points{lay.points};
@@ -4115,40 +4192,12 @@ std::vector<segment_data> editor_mode::extend_pieces() const
editor_track::snap_target target; editor_track::snap_target target;
if (extend_snap(target)) if (extend_snap(target))
{ {
alignment::design design; double length;
design.norms = m_route.design.norms; std::string error;
design.shape = m_route.design.shape; 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);
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;
} }
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]}; auto const &diverging{tool.track->m_paths[1]};
bezier const curve{diverging}; bezier const curve{diverging};
@@ -4170,26 +4219,156 @@ std::vector<segment_data> editor_mode::extend_pieces() const
return pieces; 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<segment_data> 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)}; auto const chord{glm::length(offset)};
if (chord < 0.5) if (chord < 0.5)
return result; return {};
auto const &direction{tool.direction}; if (m_track_layout == track_layout::straight)
auto const along{glm::dot(offset, direction)}; return straight_piece(Point, Direction, Grade, along);
auto const lateral{direction.x * offset.y - direction.y * offset.x}; if (m_track_layout == track_layout::arc || std::abs(lateral) < 0.005 * chord + 0.05)
auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; return arc_through(Point, Direction, Grade, offset);
if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift || std::abs(lateral) < 0.005 * chord + 0.05) 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) auto const centre{plan_of(Point) + normal * radius};
return result; auto const towards{plan_of(Mouse) - centre};
segment_data path; auto const distance{glm::length(towards)};
path.points[segment_data::point::start] = tool.point; auto const spread{std::acos(std::clamp(radius / distance, -1.0, 1.0))};
path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along}; auto const radial0{plan_of(Point) - centre};
result.push_back(path); double rest{-1.0};
return result; 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<double>();
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<double>())
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)}; auto const across{radius - std::abs(lateral)};
return arc_pieces(tool.point, direction, tool.grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1); 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<double>())
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<segment_data> 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<segment_data> 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() void editor_mode::finish_extend()
@@ -4299,8 +4478,70 @@ void editor_mode::render_extend_ui()
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f)))
extend_add(-1); extend_add(-1);
ImGui::Checkbox(STR_C("Freehand"), &m_extend_freehand); ImGui::PushID("extend");
item_tooltip("Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end"); 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<int>(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<track_layout>(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<track_layout>((static_cast<int>(m_track_layout) + 4 - step) % 4);
return true;
} }
std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const
@@ -4309,6 +4550,24 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
Length = 0.0; Length = 0.0;
if (Points.size() < 2) if (Points.size() < 2)
return {}; 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 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])}; 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) if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3)
@@ -4338,9 +4597,15 @@ std::vector<segment_data> editor_mode::lay_pieces(std::vector<glm::dvec3> const
result.pieces.clear(); result.pieces.clear();
return result; return result;
}; };
auto const in{eased(tool.start, 0.0, 1.0)}; auto const in{tool.transitions ? eased(tool.start, 0.0, 1.0) : easing{}};
auto const out{eased(End, shape.length, -1.0)}; auto const out{tool.transitions ? eased(End, shape.length, -1.0) : easing{}};
if (false == in.pieces.empty() || false == out.pieces.empty()) 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 points{Points};
auto starting{design.start_direction}; auto starting{design.start_direction};
@@ -4380,7 +4645,7 @@ alignment::result editor_mode::lay_shape(std::vector<glm::dvec3> const &Points,
design = {}; design = {};
design.norms = m_route.design.norms; design.norms = m_route.design.norms;
design.shape = m_route.design.shape; 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.start = Points.front();
design.end = Points.back(); design.end = Points.back();
design.start_direction = Startdirection; design.start_direction = Startdirection;
@@ -4388,12 +4653,6 @@ alignment::result editor_mode::lay_shape(std::vector<glm::dvec3> const &Points,
alignment::vertex corner; alignment::vertex corner;
corner.radius = std::max(1.0, tool.radius); 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 (Points.size() == 2)
{ {
if (false == alignment::collinear(design)) if (false == alignment::collinear(design))
@@ -4423,7 +4682,7 @@ alignment::result editor_mode::lay_shape(std::vector<glm::dvec3> const &Points,
design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); 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() void editor_mode::lay_click()
@@ -4447,8 +4706,31 @@ void editor_mode::lay_click()
auto const point{placement_on_ground(tool.mouse)}; auto const point{placement_on_ground(tool.mouse)};
if (plan_distance(point, tool.points.back()) < 1.0) if (plan_distance(point, tool.points.back()) < 1.0)
return; return;
tool.points.push_back(point);
m_typed.clear(); 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<TTrack *>(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) 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); 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"); 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); 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(); ImGui::PopItemWidth();
render_layout_combo();
if (false == ImGui::TreeNode(STR_C("More settings"))) if (false == ImGui::TreeNode(STR_C("More settings")))
{ {
render_lay_status(); render_lay_status();

View File

@@ -731,7 +731,7 @@ char const *editor_mode::typed_meaning() const
{ {
auto const &straights{m_straights}; auto const &straights{m_straights};
if (m_extend.active) 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) if (straights.dragging)
return straights.handle == 2 ? "type the shift, + to the right" : "type the length"; return straights.handle == 2 ? "type the shift, + to the right" : "type the length";
if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging)) if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging))
@@ -800,7 +800,9 @@ void editor_mode::typed_extend()
return; return;
auto &tool{m_extend}; auto &tool{m_extend};
auto const &direction{tool.direction}; 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])}; 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}; 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)}; 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) if (angle < 0.0)
angle += 2.0 * glm::pi<double>(); angle += 2.0 * glm::pi<double>();
angle = std::clamp(angle, 1e-3, 2.0 * glm::pi<double>() - 0.1); angle = std::clamp(angle, 1e-3, glm::pi<double>());
auto const end{centre + geometry::turned(radial, side * angle)}; auto const end{centre + geometry::turned(radial, side * angle)};
tool.mouse = {end.x, tool.mouse.y, end.y}; tool.mouse = {end.x, tool.mouse.y, end.y};
} }

View File

@@ -17,6 +17,7 @@ http:
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <functional> #include <functional>
#include <limits>
namespace alignment namespace alignment
{ {
@@ -378,6 +379,7 @@ result fit_between(design const &Design)
double tangent_in{0.0}; double tangent_in{0.0};
double tangent_out{0.0}; double tangent_out{0.0};
double deflection{0.0}; double deflection{0.0};
double cant{0.0};
bool compound{false}; bool compound{false};
std::vector<compound_arc> arcs; std::vector<compound_arc> arcs;
std::function<void(double, double &, double &)> tangents; std::function<void(double, double &, double &)> 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)); 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<double> vertexchainage(count, 0.0); std::vector<double> vertexchainage(count, 0.0);
glm::dvec2 cursor{start}; glm::dvec2 cursor{start};
@@ -650,7 +690,7 @@ result fit_between(design const &Design)
curve.vertex = static_cast<int>(k); curve.vertex = static_cast<int>(k);
curve.radius = radius; curve.radius = radius;
curve.turn = side; curve.turn = side;
curve.cant = std::max(0.0, vertex.cant); curve.cant = fit.cant;
if (lengthin > 0.0) if (lengthin > 0.0)
{ {
curve.kind = element_kind::transition_in; curve.kind = element_kind::transition_in;
@@ -804,6 +844,227 @@ result compute(design const &Design)
return r; 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<int>(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<segment_data> &Pieces, double const Start, double const End)
{
auto const count{static_cast<double>(Pieces.size())};
for (std::size_t i = 0; i < Pieces.size(); ++i)
Pieces[i].rolls = {static_cast<float>(Start + (End - Start) * i / count), static_cast<float>(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<join_result> 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<float>(a.roll);
curve.pieces.back().rolls[1] = static_cast<float>(b.roll);
curve.length = r.length;
curve.speed = shape.speed;
curve.warnings = r.warnings;
curve.radius = std::numeric_limits<double>::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<int>(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) bool tangent_intersection(design const &Design, glm::dvec2 &Point)
{ {
auto const start{plan_of(Design.start)}; auto const start{plan_of(Design.start)};

View File

@@ -35,6 +35,8 @@ struct limits
double vertical_factor{0.4}; double vertical_factor{0.4};
double vertical_min{2000.0}; double vertical_min{2000.0};
double tangent_min_time{0.0}; double tangent_min_time{0.0};
double arc_min{20.0};
double arc_min_factor{0.4};
}; };
struct compound_arc struct compound_arc
@@ -164,6 +166,8 @@ struct result
}; };
result compute(design const &Design); 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 tangent_intersection(design const &Design, glm::dvec2 &Point);
bool collinear(design const &Design); bool collinear(design const &Design);
@@ -184,4 +188,49 @@ std::size_t minimum_pieces(result const &Result, design const &Design);
std::vector<segment_data> pieces(result const &Result, design const &Design, std::size_t const Count, std::vector<double> const &Breaks = {}, std::vector<std::size_t> const &Counts = {}, std::vector<segment_data> pieces(result const &Result, design const &Design, std::size_t const Count, std::vector<double> const &Breaks = {}, std::vector<std::size_t> const &Counts = {},
std::vector<std::size_t> *Intervals = nullptr); std::vector<std::size_t> *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<segment_data> 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<std::string> warnings;
};
join_result join(join_request const &Request);
} }

View File

@@ -5213,3 +5213,73 @@ msgstr "Punkt znikania"
msgid "Pedestrian crossing" msgid "Pedestrian crossing"
msgstr "Przejście dla pieszych" 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ń"