17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-26 17:04:53 +01:00

feat(editor): the tracks laid on the terrain model go onto it as the tracks going onto the terrain do, the chain laid since the laying began fitted again at each click, with the tracks around kept and met; a straight or an arc cut by the fit before made one again, a path cut only where it would part from the line by more than 3 cm; a choice of the heights while laying: fitted, from point to point, level, kept in the settings; the fit taken back with its click

This commit is contained in:
maj00r
2026-10-23 15:55:58 +02:00
parent 7c0b3bc858
commit 4d2f95aa43
11 changed files with 392 additions and 90 deletions

View File

@@ -436,8 +436,6 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void nmt_follow_tracks();
// the height the ground will have where the terrain is made ahead of the track laid and isn't there yet
bool nmt_ahead(double const X, double const Z, double &Height) const;
// a track laid through the points gets its formation from the terrain of the model: it needn't keep to every bump of it
bool nmt_formed(std::vector<glm::dvec2> const &Points) const;
// the squares of the model under the rectangle are in, none of them waiting to be asked for again
bool nmt_ready(glm::dvec2 const &Min, glm::dvec2 const &Max);
// Only: the paths of these tracks are shaped anew, the modifiers of those removed taken away; null: all of them. the number
@@ -466,8 +464,18 @@ class editor_mode : public application_mode, private editor_track::observer, pri
std::size_t nmt_join_ends();
void nmt_auto_start();
void nmt_auto_step();
// the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be
bool nmt_fit_line(TTrack &Track);
// the grade line of the line of tracks through the track fitted to the terrain model and applied; false if it couldn't be.
// The line runs over the tracks not Fitted, the ones fitted stay as they are and the line meets them. Where the model falls
// away under the line by the abutments the tracks go over on a bridge; Laid: the tracks just laid, the formation fills it and
// they are cut only where they can't follow the line otherwise
bool nmt_fit_line(TTrack &Track, std::function<bool(TTrack const *)> const &Fitted, bool const Laid);
// the tracks laid in one go since the history step From put onto the terrain model as one line, as the tracks go onto the
// terrain: the track ending at End then the one the laying goes on from
bool nmt_fit_laid(std::size_t const From, glm::dvec3 const End);
// the tracks laid one after another on one straight or one arc, cut by the grade lines fitted before, made one again
void nmt_merge_laid(std::unordered_set<TTrack const *> &Laid);
// the points of the paths of the track about a metre apart, in squares of the plan
static void nmt_sample_paths(TTrack const &Track, std::map<std::pair<int, int>, std::vector<nmt_state::laid_point>> &Points);
// the tool of the terrain the left mouse button works
enum class terrain_tool
{
@@ -1384,6 +1392,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
glm::dvec3 point{0.0};
};
std::vector<step> chain;
std::size_t history{0}; // the step of the history the chain was begun at
};
lay_tool m_lay;
bool m_norms_free{false};
@@ -2335,7 +2344,7 @@ class editor_mode : public application_mode, private editor_track::observer, pri
void profile_check();
// Terrain: the heightmap terrain follows the tracks changed. Bridges: stretches of the route over a bridge or a viaduct, the
// paths over them cut off at their ends and set to the bridge environment
void profile_apply(bool const Terrain = false, std::vector<profile::departure> const &Bridges = {});
void profile_apply(bool const Terrain = false, std::vector<profile::departure> const &Bridges = {}, double const Fewest = 0.0);
// the terrain under the tracks changed by the last Edits steps of the history which changed tracks
void profile_follow_terrain(std::size_t const Edits);
void profile_fit_view();

View File

@@ -66,7 +66,7 @@ void editor_mode::update_build_tools()
m_extend.mouse = cursor_terrain(m_extend.point.y);
m_extend.snap = 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{};
if (m_extend.snap.track == nullptr)
m_extend.mouse.y = bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track);
m_extend.mouse.y = EditorSettings.heights() == editorSettings::lay_heights::level ? m_extend.point.y : bed_height(m_extend.mouse, plan_of(m_extend.mouse - m_extend.point), m_extend.track);
joining(m_extend.snap);
}
if (m_switch.placing)
@@ -634,12 +634,14 @@ void editor_mode::finish_extend()
give_own_speed(created);
editor_track::commit(created);
digitize_drawn(created);
auto const from{m_history.size()};
push_track_snapshot({}, created);
first_step_done(first_step::laid);
if (target.track != nullptr)
first_step_done(first_step::join);
m_node = created.back();
ui()->set_node(m_node);
nmt_fit_laid(from, created.back()->m_paths.front().points[segment_data::point::end]);
straight_refresh();
}
@@ -1122,6 +1124,8 @@ void editor_mode::lay_finish(editor_track::snap_target const &End)
give_own_speed(created);
editor_track::commit(created);
digitize_drawn(created);
if (tool.chain.empty())
tool.history = m_history.size();
push_track_snapshot({}, created);
first_step_done(first_step::laid);
if (End.track != nullptr)
@@ -1152,11 +1156,13 @@ void editor_mode::lay_finish(editor_track::snap_target const &End)
}
if (scene::Layers.active() == null_handle && style == nullptr)
tool.status += "\n" + STR("The scenery isn't opened for editing: new track won't be saved");
m_node = created.back();
ui()->set_node(m_node);
if (nmt_fit_laid(tool.history, created.back()->m_paths.front().points[segment_data::point::end]))
tool.status += "\n" + STR("The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain");
WriteLog("Editor: " + tool.status, logtype::generic);
tool.points.clear();
tool.start = {};
m_node = created.back();
ui()->set_node(m_node);
straight_refresh();
}
@@ -1522,6 +1528,16 @@ void editor_mode::render_lay_ui()
: "Off: a track laid on from another one, or joined to it, is designed for the speed of that track and takes it.\nOn: it's designed for the speed above and the new tracks get it.\nWhile laying: Ctrl+Shift+wheel");
if (auto const [from, to]{laying_ends()}; false == m_norms_free && (from != nullptr || to != nullptr))
ImGui::TextDisabled("%s", design_speed_of(from, to).c_str());
ImGui::TextDisabled("%s", STR_C("Height of the track on the ground"));
if (int heights{static_cast<int>(EditorSettings.heights())};
segmented("layheights", heights,
{{0, STR_C("Fitted"), "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past, the earthworks made by the terrain. On the terrain model the whole track laid in a row is fitted again at each click"},
{1, STR_C("Point to point"), "Each point clicked at the height of the ground, an even grade between them: the grade changes only where you click"},
{2, STR_C("Flat"), "The track goes on at the height it's laid from, whatever the ground; the first point on the ground"}}))
{
EditorSettings.heights(static_cast<editorSettings::lay_heights>(heights));
EditorSettings.save();
}
if (auto *track{selected_track()}; track != nullptr)
{
std::string reason;

View File

@@ -751,20 +751,6 @@ bool editor_mode::nmt_ahead(double const X, double const Z, double &Height) cons
return true;
}
bool editor_mode::nmt_formed(std::vector<glm::dvec2> const &Points) const
{
auto const &state{m_nmt_state};
if (false == state.shape || false == m_nmt.placed() || false == m_streamer.active())
return false;
bool foreign{false};
if (nmt_terrain_offset(Points, foreign).has_value())
return true;
if (foreign)
return false;
double height;
return std::all_of(Points.begin(), Points.end(), [&](glm::dvec2 const &Point) { return false == m_streamer.height_at(Point.x, Point.y, height) && nmt_ahead(Point.x, Point.y, height); });
}
editor_mode::nmt_chunk editor_mode::add_chunk_from_nmt(heightmap::chunk_key const &Key, std::optional<double> const Offset)
{
auto const low{heightmap::chunk_corner(Key)};
@@ -1675,11 +1661,10 @@ void editor_mode::nmt_auto_start()
state.status = STR("Fetching the terrain model under the tracks...");
}
bool editor_mode::nmt_fit_line(TTrack &Track)
bool editor_mode::nmt_fit_line(TTrack &Track, std::function<bool(TTrack const *)> const &Fitted, bool const Laid)
{
auto &profile{m_profile};
auto const &done{m_nmt_state.done};
auto const fitted = [&](TTrack const *Other) { return std::find(done.begin(), done.end(), Other) != done.end(); };
auto const &fitted{Fitted};
// the line runs over the tracks not fitted yet around the track: the ones fitted stay as they are
auto route{editor_track::run_route(Track, profile.run_length)};
auto const at{std::find_if(route.spans.begin(), route.spans.end(), [&](auto const &Span) { return Span.track == &Track; })};
@@ -2076,11 +2061,12 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
// spans a few metres apart are one bridge on its piers; what is shorter than that is a culvert under the embankment
double constexpr kPier{5.0}; // m
double constexpr kCulvert{3.0}; // m
double constexpr kFewest{0.03}; // m, how far a path just laid may part from the line before it's cut
std::vector<double> heights(count);
for (std::size_t i = 0; i < count; ++i)
heights[i] = profile::elevation(fitted_line, sampled[i]);
std::vector<profile::departure> bridges;
for (auto const &stretch : profile::bridged(sampled, heights, model, kBridged, kAbutment))
for (auto const &stretch : Laid ? std::vector<profile::departure>{} : profile::bridged(sampled, heights, model, kBridged, kAbutment))
if (false == bridges.empty() && stretch.from - bridges.back().to < kPier)
{
bridges.back().to = stretch.to;
@@ -2089,7 +2075,7 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
else
bridges.push_back(stretch);
bridges.erase(std::remove_if(bridges.begin(), bridges.end(), [&](profile::departure const &Bridge) { return Bridge.to - Bridge.from < kCulvert; }), bridges.end());
profile_apply(false, bridges);
profile_apply(false, bridges, Laid ? kFewest : 0.0);
auto const applied{profile.error.empty()};
// where the line parts from the model: under it a gap, a bridge or a viaduct, over it a wall or a structure above the track
auto const parted{profile::departures(fitted_line, sampled, model, 2.0 * kModel)};
@@ -2186,6 +2172,200 @@ bool editor_mode::nmt_fit_line(TTrack &Track)
return applied;
}
void editor_mode::nmt_sample_paths(TTrack const &Track, std::map<std::pair<int, int>, std::vector<nmt_state::laid_point>> &Points)
{
for (auto const &path : Track.m_paths)
{
geometry::bezier const curve{path};
auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])};
auto const count{std::max(1, static_cast<int>(std::ceil(chord * 1.1)))};
for (int i = 0; i <= count; ++i)
{
auto const t{static_cast<double>(i) / count};
auto const at{curve.point(t)};
auto const tangent{curve.first(t)};
auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})};
if (run > 1e-9)
Points[{static_cast<int>(std::floor(at.x / kPlanCell)), static_cast<int>(std::floor(at.z / kPlanCell))}].push_back(
{at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast<double>(Track.rail_height()), &Track});
}
}
}
void editor_mode::nmt_merge_laid(std::unordered_set<TTrack const *> &Laid)
{
auto const direction = [](segment_data const &Path) {
auto const control{geometry::plan_of(Path.points[segment_data::point::control1])};
auto const chord{geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])};
auto const along{glm::length(control) > 1e-6 ? control : chord};
return along / std::max(1e-12, glm::length(along));
};
auto const straight = [&](segment_data const &Path) {
if (std::abs(Path.radius) > 1e-3f)
return false;
auto const along{direction(Path)};
return std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start]))) < 1e-3
&& std::abs(geometry::cross(along, geometry::plan_of(Path.points[segment_data::point::control2]))) < 1e-3;
};
struct circle
{
glm::dvec2 centre{0.0};
double radius{0.0};
int side{0};
double angle{0.0};
};
auto const arc = [&](segment_data const &Path) -> std::optional<circle> {
auto const radius{std::abs(static_cast<double>(Path.radius))};
if (radius < 1e-3 || glm::length(geometry::plan_of(Path.points[segment_data::point::control1])) < 1e-6)
return std::nullopt;
auto const start{geometry::plan_of(Path.points[segment_data::point::start])};
auto const end{geometry::plan_of(Path.points[segment_data::point::end])};
auto const along{direction(Path)};
auto const side{geometry::cross(along, end - start) > 0.0 ? 1 : -1};
circle result{start + glm::dvec2{-along.y, along.x} * (side * radius), radius, side, 0.0};
if (std::abs(glm::distance(result.centre, end) - radius) > 0.01)
return std::nullopt;
result.angle = std::abs(geometry::signed_angle(start - result.centre, end - result.centre));
return result;
};
auto const alike = [](TTrack const &A, TTrack const &B) {
return editor_track::velocity(A) == editor_track::velocity(B) && A.eEnvironment == B.eEnvironment && A.iCategoryFlag == B.iCategoryFlag && A.iDamageFlag == B.iDamageFlag;
};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> removed;
for (bool merged{true}; merged;)
{
merged = false;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || false == Laid.contains(track) || track->eType != tt_Normal || track->m_paths.size() != 1)
continue;
auto *next{track->trNext};
if (next == nullptr || next == track || next->m_editorremoved || false == Laid.contains(next) || next->eType != tt_Normal || next->m_paths.size() != 1 || next->trPrev != track || false == alike(*track, *next))
continue;
auto const &first{track->m_paths.front()};
auto const &second{next->m_paths.front()};
if (glm::distance(first.points[segment_data::point::end], second.points[segment_data::point::start]) > 1e-3)
continue;
auto const length{geometry::bezier(first).plan_length() + geometry::bezier(second).plan_length()};
if (length > geometry::longest_path)
continue;
auto path{first};
path.pieces.clear();
if (straight(first) && straight(second) && std::abs(geometry::cross(direction(first), direction(second))) < 1e-5 && glm::dot(direction(first), direction(second)) > 0.0)
{
path.points[segment_data::point::control1] = {};
path.points[segment_data::point::control2] = {};
path.radius = 0.f;
}
else if (auto const a{arc(first)}, b{arc(second)};
a && b && a->side == b->side && std::abs(a->radius - b->radius) < 1e-3 * a->radius && glm::distance(a->centre, b->centre) < 0.01 && a->angle + b->angle <= glm::radians(90.0))
{
auto const pieces{geometry::arc_pieces(first.points[segment_data::point::start], direction(first), 0.0, a->radius, a->angle + b->angle, a->side)};
if (pieces.size() != 1)
continue;
path.points[segment_data::point::control1] = pieces.front().points[segment_data::point::control1];
path.points[segment_data::point::control2] = pieces.front().points[segment_data::point::control2];
}
else
continue;
path.points[segment_data::point::end] = second.points[segment_data::point::end];
path.rolls[1] = second.rolls[1];
if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; }))
states.emplace_back(track, editor_track::capture(*track));
editor_track::retire(*next);
Laid.erase(next);
removed.push_back(next);
track->m_paths.front() = path;
editor_track::commit({track});
merged = true;
}
}
if (false == removed.empty())
push_track_snapshot(std::move(states), {}, std::move(removed));
}
bool editor_mode::nmt_fit_laid(std::size_t const From, glm::dvec3 const End)
{
auto &state{m_nmt_state};
if (EditorSettings.heights() != editorSettings::lay_heights::fitted || state.stage != nmt_state::automatic::idle || false == m_nmt.placed() || From >= m_history.size())
return false;
std::unordered_set<TTrack const *> laid;
for (auto step{From}; step < m_history.size(); ++step)
for (auto const *track : m_history[step].created)
if (track != nullptr && false == track->m_editorremoved && rail(*track))
laid.insert(track);
auto const at_end = [&]() -> TTrack * {
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && laid.contains(track) && track->eType == tt_Normal && geometry::plan_distance(track->m_paths.front().points[segment_data::point::end], End) < 0.01)
return track;
return nullptr;
};
auto const steps{m_history.size()};
nmt_merge_laid(laid);
auto *last{at_end()};
if (last == nullptr)
return false;
// the tracks around stay as they are, as lines fitted already: the line keeps level with the ones beside it, the more closely
// the nearer they are, and crosses the ones it crosses at their height
double constexpr kAround{60.0}; // m
glm::dvec2 low{std::numeric_limits<double>::max()};
glm::dvec2 high{std::numeric_limits<double>::lowest()};
for (auto const *track : laid)
for (auto const &path : track->m_paths)
for (auto const end : {segment_data::point::start, segment_data::point::end})
{
low = glm::min(low, geometry::plan_of(path.points[end]));
high = glm::max(high, geometry::plan_of(path.points[end]));
}
low -= kAround;
high += kAround;
std::map<std::pair<int, int>, std::vector<nmt_state::laid_point>> around;
for (auto *track : simulation::Paths.sequence())
{
if (track == nullptr || track->m_editorremoved || false == rail(*track) || laid.contains(track))
continue;
auto const &points{track->m_paths.front().points};
auto const a{geometry::plan_of(points[segment_data::point::start])};
auto const b{geometry::plan_of(points[segment_data::point::end])};
if (std::max(a.x, b.x) < low.x || std::min(a.x, b.x) > high.x || std::max(a.y, b.y) < low.y || std::min(a.y, b.y) > high.y)
continue;
nmt_sample_paths(*track, around);
}
// the state of the tracks going onto the terrain lent to the one line, the profile tool left as it was
auto const profile{m_profile};
auto fitted_lines{std::exchange(state.laid, around)};
auto paths{std::exchange(state.paths, std::move(around))};
auto loose{std::exchange(state.loose, {})};
auto const departures{state.departures};
auto const applied{nmt_fit_line(*last, [&](TTrack const *Other) { return false == laid.contains(Other); }, true)};
auto const error{m_profile.error};
state.laid = std::move(fitted_lines);
state.paths = std::move(paths);
state.loose = std::move(loose);
state.departures = departures;
m_profile = profile;
// taken back with the laying
for (auto step{steps}; step < m_history.size(); ++step)
m_history[step].joined = true;
if (applied)
{
laid.clear();
for (auto step{From}; step < m_history.size(); ++step)
for (auto const *track : m_history[step].created)
if (track != nullptr && false == track->m_editorremoved)
laid.insert(track);
if (auto *track{at_end()}; track != nullptr)
{
m_node = track;
ui()->set_node(m_node);
}
}
else if (false == error.empty())
WriteLog("Editor: the track laid not put onto the terrain model - " + error, logtype::generic);
return applied;
}
void editor_mode::nmt_auto_step()
{
auto &state{m_nmt_state};
@@ -2247,22 +2427,7 @@ void editor_mode::nmt_auto_step()
state.paths.clear();
for (auto *track : simulation::Paths.sequence())
if (track != nullptr && rail(*track))
for (auto const &path : track->m_paths)
{
geometry::bezier const curve{path};
auto const chord{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])};
auto const count{std::max(1, static_cast<int>(std::ceil(chord * 1.1)))};
for (int i = 0; i <= count; ++i)
{
auto const t{static_cast<double>(i) / count};
auto const at{curve.point(t)};
auto const tangent{curve.first(t)};
auto const run{glm::length(glm::dvec2{tangent.x, tangent.z})};
if (run > 1e-9)
state.paths[{static_cast<int>(std::floor(at.x / kPlanCell)), static_cast<int>(std::floor(at.z / kPlanCell))}].push_back(
{at, glm::dvec2{tangent.x, tangent.z} / run, tangent.y / run, at.y, static_cast<double>(track->rail_height()), track});
}
}
nmt_sample_paths(*track, state.paths);
state.stage = nmt_state::automatic::fitting;
return;
}
@@ -2289,7 +2454,7 @@ void editor_mode::nmt_auto_step()
auto *track{state.left.front()};
auto const name{track->name()};
auto const known{simulation::Paths.sequence().size()};
if (nmt_fit_line(*track))
if (nmt_fit_line(*track, [&](TTrack const *Other) { return fitted.contains(Other); }, false))
++state.lines;
else
++state.refused;

View File

@@ -1450,7 +1450,7 @@ void editor_mode::profile_frame_view()
fly_to(centre - right * (shift.x * pixel) + above * (shift.y * pixel), look, distance, 0.35);
}
void editor_mode::profile_apply(bool const Terrain, std::vector<profile::departure> const &Bridges)
void editor_mode::profile_apply(bool const Terrain, std::vector<profile::departure> const &Bridges, double const Fewest)
{
auto &state{m_profile};
state.status.clear();
@@ -1475,12 +1475,14 @@ void editor_mode::profile_apply(bool const Terrain, std::vector<profile::departu
auto const before{state.samples};
std::vector<std::pair<TTrack *, editor_track::state>> states;
std::vector<TTrack *> created;
auto cuts{profile::breaks(line)};
std::vector<double> ends;
for (auto const &span : state.route.spans)
ends.insert(ends.end(), {span.from, span.to});
auto cuts{Fewest > 0.0 ? profile::fewest_breaks(line, ends, Fewest) : profile::breaks(line)};
// a bridge ends at a joint or a break of the line by its abutment rather than leave a sliver of a path there
double constexpr kBridgeSnap{1.0}; // m
auto joints{cuts};
for (auto const &span : state.route.spans)
joints.insert(joints.end(), {span.from, span.to});
joints.insert(joints.end(), ends.begin(), ends.end());
auto bridges{Bridges};
for (auto &bridge : bridges)
for (auto *end : {&bridge.from, &bridge.to})

View File

@@ -129,6 +129,14 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
WriteLog(format("SELFTEST %s end: %.3f m from the target", command.c_str(), geometry::plan_distance(previous, b)));
return selftest_result::handled;
}
if (command == "layheights")
{
std::string which;
words >> which;
EditorSettings.heights(which == "points" ? editorSettings::lay_heights::points : which == "level" ? editorSettings::lay_heights::level : editorSettings::lay_heights::fitted);
WriteLog("SELFTEST layheights: " + which);
return selftest_result::handled;
}
if (command == "norms")
{
std::string which;
@@ -237,34 +245,47 @@ editor_mode::selftest_result editor_mode::selftest_join(std::string const &comma
double hanging{0.0};
double buried{0.0};
int paths{0};
// the paths laid as they lie now, along the line: a grade line fitted afterwards cut some of them
std::vector<TTrack const *> laid;
for (auto step{before}; step < m_history.size(); ++step)
for (auto const *track : m_history[step].created)
if (track != nullptr && false == track->m_editorremoved && std::find(laid.begin(), laid.end(), track) == laid.end())
laid.push_back(track);
if (false == laid.empty())
{
auto const origin{laid.front()->m_paths.front().points[segment_data::point::start]};
auto const along{glm::normalize(geometry::plan_of(laid.back()->m_paths.front().points[segment_data::point::end] - origin) + glm::dvec2{1e-9, 0.0})};
std::stable_sort(laid.begin(), laid.end(), [&](TTrack const *A, TTrack const *B) {
return glm::dot(geometry::plan_of(A->m_paths.front().points[segment_data::point::start] - origin), along) < glm::dot(geometry::plan_of(B->m_paths.front().points[segment_data::point::start] - origin), along);
});
}
for (auto const *track : laid)
{
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 &a{path.points[segment_data::point::start]};
auto const &b{path.points[segment_data::point::end]};
auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); };
WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast<double>(path.radius),
first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)),
first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope)));
slope = rise(curve.first(1.0));
heading = glm::normalize(geometry::plan_of(curve.first(1.0)));
previous = b;
first = false;
auto const count{std::max(1, static_cast<int>(std::ceil(curve.plan_length() / 2.0)))};
for (int k = 0; k <= count; ++k)
{
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 &a{path.points[segment_data::point::start]};
auto const &b{path.points[segment_data::point::end]};
auto const rise = [](glm::dvec3 const &Tangent) { return Tangent.y / std::max(1e-9, glm::length(geometry::plan_of(Tangent))); };
WriteLog(format("SELFTEST layclicks path %s: %.2f %.2f -> %.2f %.2f, L %.2f m, R %.1f m, kink %.4f deg, step %.4f m, y %.3f -> %.3f, grade %.2f -> %.2f permille, grade break %.3f permille", track->name().c_str(), a.x, a.z, b.x, b.z, curve.plan_length(), static_cast<double>(path.radius),
first ? 0.0 : glm::degrees(std::abs(geometry::signed_angle(heading, begin))), first ? 0.0 : glm::distance(previous, a), a.y, b.y, 1000.0 * rise(curve.first(0.0)), 1000.0 * rise(curve.first(1.0)),
first ? 0.0 : 1000.0 * std::abs(rise(curve.first(0.0)) - slope)));
slope = rise(curve.first(1.0));
heading = glm::normalize(geometry::plan_of(curve.first(1.0)));
previous = b;
first = false;
auto const count{std::max(1, static_cast<int>(std::ceil(curve.plan_length() / 2.0)))};
for (int k = 0; k <= count; ++k)
{
auto const t{curve.parameter(curve.plan_length() * k / count)};
auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()};
if (std::isnan(target))
continue;
hanging = std::max(hanging, curve.point(t).y - target);
buried = std::max(buried, target - curve.point(t).y);
}
++paths;
auto const t{curve.parameter(curve.plan_length() * k / count)};
auto const target{bed_heights({curve.point(t) + glm::dvec3{0.0, 50.0, 0.0}}, {geometry::plan_of(curve.first(t))}, track).front()};
if (std::isnan(target))
continue;
hanging = std::max(hanging, curve.point(t).y - target);
buried = std::max(buried, target - curve.point(t).y);
}
++paths;
}
WriteLog(format("SELFTEST layclicks %d paths, ballast foot over the ground up to %.3f m, under it up to %.3f m", paths, hanging, buried));
return selftest_result::handled;
}

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "application/editormode.h"
#include "editor/editorGeometry.hpp"
#include "editor/editorGroundFollow.hpp"
#include "editor/editorSettings.hpp"
#include "utilities/Globals.h"
#include "world/Track.h"
@@ -26,9 +27,7 @@ using geometry::plan_distance;
using geometry::plan_of;
double constexpr kSteepest{1000.0}; // per mille: the grades of the user's own track aren't capped
double constexpr kFormed{0.5}; // m, how closely the line keeps to the ground the formation is made in
double constexpr kBare{0.1}; // m, to the ground left as it is
double constexpr kBareRadius{300.0}; // m, the vertical curves over it
double constexpr kModel{0.15}; // m, how closely the line keeps to the ground, as to the terrain model
} // namespace
@@ -129,35 +128,35 @@ double editor_mode::bed_height(glm::dvec3 const &Point, glm::dvec2 const &Direct
void editor_mode::follow_ground(std::vector<segment_data> &Pieces, double const Height0, double const Grade0, double const Height1, double const Grade1, TTrack const *Style) const
{
std::vector<glm::dvec2> points;
for (auto const &piece : Pieces)
// from point to point or level the heights lead from one end to the other, whatever the ground between
if (EditorSettings.heights() != editorSettings::lay_heights::fitted)
{
geometry::bezier const curve{piece};
auto const count{std::max(6, static_cast<int>(std::ceil(curve.plan_length() / 10.0)))};
for (int i = 0; i <= count; ++i)
points.push_back(plan_of(curve.point(static_cast<double>(i) / count)));
ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [](std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &) { return std::vector<double>(Points.size(), std::numeric_limits<double>::quiet_NaN()); });
return;
}
// the ground the formation is made in lets the line keep to the norms of the profile, cutting and filling; the ground left as
// it is the line follows, bending as sharply as it has to
auto const formed{nmt_formed(points)};
// the line goes over the ground as the tracks go onto the terrain: the vertical curves of the norms, the grades as steep as the
// ground has them, a rise or a hollow longer than a structure followed closely, the bumps and what parts from it gone past
ground_follow::grading grading;
grading.settings.norms = m_profile.line.norms;
grading.settings.norms.grade_max = kSteepest;
grading.settings.norms.curve_threshold = 0.0;
if (false == formed)
{
grading.settings.norms.radius_factor = 0.0;
grading.settings.norms.radius_min = kBareRadius;
}
grading.settings.speed = design_speed(Style, nullptr);
grading.fit.grade_max = kSteepest;
grading.fit.tolerance = formed ? kFormed : kBare;
grading.fit.tolerance = kModel;
grading.fit.follow_length = 60.0;
grading.fit.follow_height = 4.0;
grading.fit.follow_steepest = 100.0;
grading.fit.follow_tolerance = 0.05;
grading.reach = kModel;
ground_follow::follow(Pieces, {Height0, Grade0, Height1, Grade1}, [&](std::vector<glm::dvec3> const &Points, std::vector<glm::dvec2> const &Directions) { return bed_heights(Points, Directions, Style); }, grading);
}
glm::dvec3 editor_mode::lay_bed(glm::dvec3 const &Point)
{
auto const &tool{m_lay};
// level: on at the height the laying goes on from, the first point on the ground
if (EditorSettings.heights() == editorSettings::lay_heights::level && false == tool.points.empty())
return {Point.x, tool.points.back().y, Point.z};
TTrack const *style{tool.start.track != nullptr ? tool.start.track : tool.copy_selected ? selected_track() : nullptr};
return {Point.x, bed_height(Point, tool.points.empty() ? glm::dvec2{0.0} : plan_of(Point - tool.points.back()), style), Point.z};
}

View File

@@ -1203,6 +1203,55 @@ std::vector<double> breaks(line const &Line)
return result;
}
std::vector<double> fewest_breaks(line const &Line, std::vector<double> Joints, double const Tolerance)
{
std::sort(Joints.begin(), Joints.end());
Joints.erase(std::unique(Joints.begin(), Joints.end(), [](double const A, double const B) { return std::abs(A - B) < 1e-6; }), Joints.end());
auto const all{breaks(Line)};
auto const inward = [&](double const Chainage, double const Other) { return grade(Line, Chainage + std::min(1e-3, std::abs(Other - Chainage) * 0.5) * (Other > Chainage ? 1.0 : -1.0)); };
auto const follows = [&](double const From, double const To) {
auto const length{To - From};
auto const h0{elevation(Line, From)};
auto const h1{elevation(Line, To)};
auto const g0{inward(From, To) * length};
auto const g1{inward(To, From) * length};
auto const count{std::max(4, static_cast<int>(std::ceil(length)))};
for (int i = 1; i < count; ++i)
{
auto const u{static_cast<double>(i) / count};
auto const u2{u * u};
auto const u3{u2 * u};
auto const height{(2.0 * u3 - 3.0 * u2 + 1.0) * h0 + (u3 - 2.0 * u2 + u) * g0 + (-2.0 * u3 + 3.0 * u2) * h1 + (u3 - u2) * g1};
if (std::abs(height - elevation(Line, From + u * length)) > Tolerance)
return false;
}
return true;
};
std::vector<double> result;
for (std::size_t k = 0; k + 1 < Joints.size(); ++k)
{
std::vector<double> stops;
for (auto const chainage : all)
if (chainage > Joints[k] + 1e-6 && chainage < Joints[k + 1] - 1e-6)
stops.push_back(chainage);
stops.push_back(Joints[k + 1]);
auto from{Joints[k]};
std::size_t next{0};
while (next + 1 < stops.size())
{
auto reach{next};
for (auto i{next + 1}; i < stops.size() && follows(from, stops[i]); ++i)
reach = i;
if (reach + 1 == stops.size())
break;
result.push_back(stops[reach]);
from = stops[reach];
next = reach + 1;
}
}
return result;
}
std::string format_chainage(double const Chainage)
{
auto const value{std::abs(Chainage)};

View File

@@ -179,6 +179,9 @@ std::vector<departure> bridged(std::vector<double> const &Chainages, std::vector
// chainages where the paths have to end so that each of them carries a single grade or a single curve
std::vector<double> breaks(line const &Line);
// of the breaks those the paths between Joints can't do without: a path from one joint or break kept to the next follows the line
// within Tolerance with the heights and the grades of its ends
std::vector<double> fewest_breaks(line const &Line, std::vector<double> Joints, double const Tolerance);
std::string format_chainage(double const Chainage);

View File

@@ -116,6 +116,8 @@ bool editorSettings::load()
m_top_rails = static_cast<std::uint32_t>(std::strtoul(value.c_str(), nullptr, 16)) | 0xff000000u;
else if (key == "track_norms")
m_norms = value == "free" ? track_norms::free_laying : value == "plk" ? track_norms::plk : track_norms::ask;
else if (key == "lay_heights")
m_heights = value == "points" ? lay_heights::points : value == "level" ? lay_heights::level : lay_heights::fitted;
else if (key.rfind(origin_prefix, 0) == 0)
{
// northing;easting
@@ -165,6 +167,7 @@ bool editorSettings::save()
stream << "top_tracks " << m_top_tracks << "\n";
stream << "top_rails " << std::hex << (m_top_rails & 0xffffffu) << std::dec << "\n";
stream << "track_norms " << (m_norms == track_norms::free_laying ? "free" : m_norms == track_norms::plk ? "plk" : "ask") << "\n";
stream << "lay_heights " << (m_heights == lay_heights::points ? "points" : m_heights == lay_heights::level ? "level" : "fitted") << "\n";
stream << std::fixed << std::setprecision(3);
for (auto const &origin : m_orthophoto_origins)
stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n";

View File

@@ -76,6 +76,16 @@ public:
};
track_norms norms() const { return m_norms; }
void norms(track_norms Norms) { m_norms = Norms; }
// the heights of the tracks laid on the ground: fitted to it as the tracks go onto the terrain, the points clicked on it with
// even grades between them, level
enum class lay_heights
{
fitted,
points,
level
};
lay_heights heights() const { return m_heights; }
void heights(lay_heights Heights) { m_heights = Heights; }
private:
movement_scheme m_movement{movement_scheme::wsad};
@@ -92,6 +102,7 @@ private:
int m_top_tracks{0}; // the railway in the top view: whole, without the ballast, the rails alone
std::uint32_t m_top_rails{0xffff6e28}; // colour of the rails alone, as ImGui keeps it: ABGR
track_norms m_norms{track_norms::ask};
lay_heights m_heights{lay_heights::fitted};
};
// global editor settings instance

View File

@@ -11502,3 +11502,27 @@ msgstr "Kolor szyn, żeby odcinały się od ortofotomapy i mapy"
msgid "(c) OpenStreetMap contributors, geoportal.gov.pl"
msgstr "(c) współtwórcy OpenStreetMap, geoportal.gov.pl"
msgid "The grade line of the whole track laid in a row fitted to the terrain model, as the tracks go onto the terrain"
msgstr "Pochylenia całego toru kładzionego za jednym razem dopasowane do modelu terenu, tak jak w „Tory na teren”"
msgid "Height of the track on the ground"
msgstr "Wysokość toru na terenie"
msgid "Fitted"
msgstr "Dopasowana"
msgid "Point to point"
msgstr "Punkt–punkt"
msgid "Flat"
msgstr "Poziomo"
msgid "The grades fitted to the ground as Tracks onto the terrain fits them: vertical curves of the norms, the bumps of the ground gone past, the earthworks made by the terrain. On the terrain model the whole track laid in a row is fitted again at each click"
msgstr "Pochylenia dopasowane do terenu tak jak w „Tory na teren”: łuki pionowe według norm, drobne nierówności terenu pominięte, nasypy i wykopy robi teren. Na modelu terenu (NMT) cały tor kładziony za jednym razem jest dopasowywany od nowa przy każdym kliknięciu"
msgid "Each point clicked at the height of the ground, an even grade between them: the grade changes only where you click"
msgstr "Każdy kliknięty punkt na wysokości terenu, między punktami równe pochylenie: pochylenie zmienia się tylko tam, gdzie klikniesz"
msgid "The track goes on at the height it's laid from, whatever the ground; the first point on the ground"
msgstr "Tor idzie dalej na wysokości, z której go kładziesz, bez względu na teren; pierwszy punkt na terenie"