17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-03 21:12:07 +01:00

feat(terrain): grade line fitted to the ground under the route within the limits; editor terrain shaped to the grade line with a formation and rounded slopes, with one step back; scenery ground triangles of chosen materials converted to editor terrain and taken out of the files on save, with a dated backup of each file; crack-free simplified terrain mesh, terrain streamed as far as the scenery is drawn, the nearest chunks first; smoothing brush

This commit is contained in:
maj00r
2026-10-05 02:21:55 +02:00
parent 3731520e33
commit a13a5548df
14 changed files with 1253 additions and 30 deletions

View File

@@ -16,6 +16,8 @@ http://mozilla.org/MPL/2.0/.
#include "editor/editorModelSets.hpp"
#include "editor/editorIncludeInfo.hpp"
#include "editor/editorGroundMesh.hpp"
#include "editor/editorFormat.hpp"
#include "utilities/translation.h"
#include "utilities/Globals.h"
#include "simulation/simulation.h"
#include "simulation/simulationtime.h"
@@ -1308,7 +1310,9 @@ void editor_mode::render_settings()
ImGui::Checkbox("Transform gizmo (ImGuizmo)", &m_gizmo_enabled);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Terrain"))
bool const terrainwanted = m_terrain_tab_wanted;
m_terrain_tab_wanted = false;
if (ImGui::BeginTabItem("Terrain", nullptr, terrainwanted ? ImGuiTabItemFlags_SetSelected : 0))
{
render_terrain_ui();
ImGui::EndTabItem();
@@ -1439,9 +1443,16 @@ void editor_mode::render_terrain_ui()
if (!terrains.empty())
{
ImGui::Separator();
if (ImGui::Checkbox("Sculpt mode (LMB raise / Shift = lower)", &m_terrain_sculpt))
if (ImGui::Checkbox((std::string(m_terrain_brush_smooth ? STR_C("Sculpt mode (LMB smooth)") : STR_C("Sculpt mode (LMB raise / Shift = lower)")) + "###sculptmode").c_str(), &m_terrain_sculpt))
if (m_terrain_sculpt)
m_chunk_edit = false; // mutually exclusive with chunk editing
int brush = m_terrain_brush_smooth ? 1 : 0;
ImGui::RadioButton(STR_C("Raise / lower"), &brush, 0);
ImGui::SameLine();
ImGui::RadioButton(STR_C("Smooth"), &brush, 1);
m_terrain_brush_smooth = (brush == 1);
if (m_terrain_brush_smooth && ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Evens out the ground under the brush: bumps and sharp edges go, the shape stays.\nThe strength sets how fast"));
ImGui::SetNextItemWidth(120.0f);
ImGui::InputFloat("Brush radius", &m_terrain_brush_radius);
if (m_terrain_brush_radius < 0.5f)
@@ -1737,6 +1748,235 @@ void editor_mode::add_grid_chunk(int Cx, int Cz)
m_grid_chunks[key] = std::move(terrain);
}
namespace
{
std::uint64_t geometry_key(gfx::geometry_handle const &Geometry)
{
return (static_cast<std::uint64_t>(Geometry.bank) << 32) | Geometry.chunk;
}
// the shapes of the scenery which can be ground: the ones the editor makes itself (its terrain, roads, imagery) left out
template <typename Visitor> void visit_scenery_shapes(std::set<std::uint64_t> const &Own, Visitor const &Visit)
{
auto const scenery = [&](scene::shape_node const &Shape) {
auto const &data = Shape.data();
return Own.count(geometry_key(data.geometry)) == 0 && false == editor_orthophoto::owns(data.geometry) && false == owned_shapes::owns(data.geometry);
};
for (std::size_t i = 0; i < sq(scene::EU07_REGIONSIDESECTIONCOUNT); ++i)
{
auto *section = simulation::Region->get_section(i);
if (section == nullptr)
continue;
for (auto &shape : section->m_shapes)
if (scenery(shape))
Visit(shape);
for (auto &cell : section->m_cells)
for (auto &shape : cell.m_shapesopaque)
if (scenery(shape))
Visit(shape);
}
}
// takes the shapes of specified materials out of the scene
std::size_t drop_scenery_shapes(std::set<std::uint64_t> const &Own, std::set<material_handle> const &Materials)
{
std::size_t dropped = 0;
auto const dropping = [&](scene::shape_node const &Shape) {
auto const &data = Shape.data();
bool const drop = Materials.count(data.material) != 0 && Own.count(geometry_key(data.geometry)) == 0 && false == editor_orthophoto::owns(data.geometry) &&
false == owned_shapes::owns(data.geometry);
dropped += drop ? 1 : 0;
return drop;
};
for (std::size_t i = 0; i < sq(scene::EU07_REGIONSIDESECTIONCOUNT); ++i)
{
auto *section = simulation::Region->get_section(i);
if (section == nullptr)
continue;
section->m_shapes.erase(std::remove_if(section->m_shapes.begin(), section->m_shapes.end(), dropping), section->m_shapes.end());
for (auto &cell : section->m_cells)
cell.m_shapesopaque.erase(std::remove_if(cell.m_shapesopaque.begin(), cell.m_shapesopaque.end(), dropping), cell.m_shapesopaque.end());
}
return dropped;
}
} // namespace
std::map<material_handle, std::size_t> editor_mode::ground_materials()
{
std::map<material_handle, std::size_t> result;
if (simulation::Region == nullptr)
return result;
std::set<std::uint64_t> own;
for (auto *terrain : active_terrains())
own.insert(geometry_key(terrain->geometry()));
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5};
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) {
std::vector<world_triangle> triangles;
gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true);
if (false == triangles.empty())
result[Shape.data().material] += triangles.size();
});
return result;
}
std::string editor_mode::convert_ground(std::set<material_handle> const &Materials)
{
if (simulation::Region == nullptr || Materials.empty())
return {};
bool const streaming = m_streamer.active();
double const size = streaming ? m_streamer.cells() * m_streamer.cellsize() : chunk_grid_size();
int const cells = streaming ? m_streamer.cells() : std::clamp(m_terrain_cells, 1, 256);
double const cellsize = size / cells;
if (size <= 0.0)
return {};
std::set<std::uint64_t> own;
for (auto *terrain : active_terrains())
own.insert(geometry_key(terrain->geometry()));
std::vector<world_triangle> triangles;
std::vector<material_handle> materials;
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5};
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) {
if (Materials.count(Shape.data().material) == 0)
return;
gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true);
materials.resize(triangles.size(), Shape.data().material);
});
if (triangles.empty())
return STR_C("The chosen materials have no triangles");
// the triangles reaching each chunk
std::map<std::pair<int, int>, std::vector<std::uint32_t>> buckets;
for (std::uint32_t i = 0; i < triangles.size(); ++i)
{
auto const &t = triangles[i];
int const x0 = static_cast<int>(std::floor(std::min({t[0].x, t[1].x, t[2].x}) / size));
int const x1 = static_cast<int>(std::floor(std::max({t[0].x, t[1].x, t[2].x}) / size));
int const z0 = static_cast<int>(std::floor(std::min({t[0].z, t[1].z, t[2].z}) / size));
int const z1 = static_cast<int>(std::floor(std::max({t[0].z, t[1].z, t[2].z}) / size));
for (int x = x0; x <= x1; ++x)
for (int z = z0; z <= z1; ++z)
buckets[{x, z}].push_back(i);
}
// the terrain of the editor has a single texture, saved with it: the material which covers the most
std::map<material_handle, double> coverage;
for (std::size_t i = 0; i < triangles.size(); ++i)
{
auto const &t = triangles[i];
coverage[materials[i]] += 0.5 * std::abs((t[1].x - t[0].x) * (t[2].z - t[0].z) - (t[2].x - t[0].x) * (t[1].z - t[0].z));
}
auto const dominant = std::max_element(coverage.begin(), coverage.end(), [](auto const &A, auto const &B) { return A.second < B.second; })->first;
if (auto const *material = GfxRenderer->Material(dominant))
{
auto const name = material->GetName();
std::snprintf(m_terrain_texture, sizeof(m_terrain_texture), "%s", name.c_str());
}
std::string const texture(m_terrain_texture);
// a whole scenery of the full grid would be a lot to draw
m_terrain_auto_optimize = true;
if (streaming)
{
m_streamer.simplify(true, m_terrain_simplify_error);
m_streamer.configure(m_streamer.cells(), m_streamer.cellsize(), m_streamer.radius(), m_terrain_baseheight, texture);
}
// the old ground goes first, so what's made in its place isn't taken for it
auto const dropped = drop_scenery_shapes(own, Materials);
std::size_t made = 0, skipped = 0;
std::size_t const side = static_cast<std::size_t>(cells) + 1;
for (auto const &bucket : buckets)
{
auto const &key = bucket.first;
glm::dvec2 const low{key.first * size, key.second * size};
glm::dvec2 const middle{low + glm::dvec2{size * 0.5}};
if (streaming ? m_streamer.terrain_at(middle.x, middle.y) != nullptr : m_grid_chunks.count(key) > 0)
{
++skipped;
continue;
}
std::vector<world_triangle> local;
for (auto const i : bucket.second)
local.push_back(triangles[i]);
triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0});
std::vector<double> heights(side * side, 0.0);
std::vector<char> found(side * side, 0);
std::vector<std::size_t> known;
for (std::size_t iz = 0; iz < side; ++iz)
for (std::size_t ix = 0; ix < side; ++ix)
{
auto const index = iz * side + ix;
if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, std::numeric_limits<double>::max(), heights[index]))
{
found[index] = 1;
known.push_back(index);
}
}
if (known.empty())
continue;
// at the edge of the ground the chunk is carried on level with the nearest point of it
if (known.size() < heights.size())
for (std::size_t index = 0; index < heights.size(); ++index)
{
if (found[index] != 0)
continue;
double best = std::numeric_limits<double>::max();
for (auto const other : known)
{
double const dx = static_cast<double>(index % side) - static_cast<double>(other % side);
double const dz = static_cast<double>(index / side) - static_cast<double>(other / side);
if (dx * dx + dz * dz < best)
{
best = dx * dx + dz * dz;
heights[index] = heights[other];
}
}
}
auto const sampler = [&](double X, double Z, double &OutY) {
auto const ix = static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / cellsize), 0, cells));
auto const iz = static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / cellsize), 0, cells));
OutY = heights[iz * side + ix];
return true;
};
if (streaming)
{
m_streamer.add_chunk(key.first, key.second);
if (auto *terrain = m_streamer.terrain_at(middle.x, middle.y))
{
terrain->reshape([&](double X, double Z, float &Height) {
double y;
sampler(X, Z, y);
Height = static_cast<float>(y);
return true;
});
++made;
}
}
else
{
auto chunk = std::make_unique<editor_terrain>();
if (chunk->create(glm::dvec3{middle.x, m_terrain_baseheight, middle.y}, cells, static_cast<float>(cellsize), texture, sampler))
{
// the full grid everywhere would be a lot to draw; the simplified one keeps it exact where it matters
chunk->optimize(m_terrain_simplify_error);
m_grid_chunks[key] = std::move(chunk);
++made;
}
}
}
ground_tiles.clear();
forget_ground_shapes();
auto const erased = scene::Layers.erase_shapes(Materials);
auto summary = format(STR_C("Ground converted: %zu chunks of editor terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the scenery files on save"), made, dropped, erased.first);
if (skipped > 0)
summary += format(STR_C(", %zu chunks kept as there was editor terrain already"), skipped);
if (erased.second > 0)
summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second);
WriteLog("Editor: " + summary, logtype::generic);
return summary;
}
void editor_mode::remove_grid_chunk(int Cx, int Cz)
{
auto const it = m_grid_chunks.find({Cx, Cz});
@@ -1875,12 +2115,14 @@ void editor_mode::save()
std::vector<std::string> rootstatements;
if (m_streamer.active() || false == m_grid_chunks.empty())
{
// the chunks go over to the streamer, what was kept to take back the shaping refers to the ones which are gone
m_profile.earthworks.undo.clear();
commit_terrain();
if (false == scene::Layers.terrain_directive())
{
rootstatements.emplace_back(
"editorterrain " + m_streamer.directory() + ' ' + std::to_string(m_streamer.cells()) + ' ' + to_string(m_streamer.cellsize(), 3) + ' '
+ std::to_string(m_streamer.radius()) + " endeditorterrain");
+ std::to_string(m_streamer.radius()) + (m_streamer.texture().empty() ? "" : ' ' + m_streamer.texture()) + " endeditorterrain");
}
}
@@ -2087,11 +2329,37 @@ void editor_mode::handle_terrain_sculpt(double Deltatime)
return;
double const rate = m_terrain_brush_strength * Deltatime; // metres applied this frame
double const signedrate = Global.shiftState ? -rate : rate;
// apply to every chunk the brush touches; each patch clips to its own bounds, so a stroke
// crossing a chunk boundary edits both and shared-edge vertices stay in sync
for (editor_terrain *terrain : active_terrains())
terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate);
if (m_terrain_brush_smooth)
{
// every patch the brush touches is smoothed from the same ground, so the edges they share stay together
auto const terrains = active_terrains();
double const reach = m_terrain_brush_radius + 2.0 * m_terrain_cellsize + 1.0;
std::vector<std::pair<editor_terrain *, std::vector<float>>> before;
for (editor_terrain *terrain : terrains)
{
auto const centre = terrain->centre();
double const half = terrain->extent() * 0.5;
if (std::abs(centre.x - world.x) <= half + reach && std::abs(centre.z - world.z) <= half + reach)
before.emplace_back(terrain, terrain->heights());
}
auto const ground = [&](double X, double Z, double Fallback) {
for (auto const &entry : before)
if (entry.first->contains(X, Z))
return entry.first->height_in(entry.second, X, Z);
return Fallback;
};
double const amount = std::min(1.0, rate * 0.5);
for (auto const &entry : before)
entry.first->smooth(world.x, world.z, m_terrain_brush_radius, amount, ground);
}
else
{
double const signedrate = Global.shiftState ? -rate : rate;
// apply to every chunk the brush touches; each patch clips to its own bounds, so a stroke
// crossing a chunk boundary edits both and shared-edge vertices stay in sync
for (editor_terrain *terrain : active_terrains())
terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate);
}
// what the road tools know about the ground is no longer true
ground_tiles.clear();
forget_ground_shapes();

View File

@@ -198,6 +198,19 @@ class editor_mode : public application_mode, private editor_track::observer
float chunk_grid_size() const { return m_terrain_cells * m_terrain_cellsize; }
void add_grid_chunk(int Cx, int Cz);
void remove_grid_chunk(int Cx, int Cz);
// the scenery ground: triangles of each material, the ones the editor makes itself left out
std::map<material_handle, std::size_t> ground_materials();
// replaces the triangles of specified materials with chunks of editor terrain of their shape; the scenery files lose them on save.
// returns: summary for the user
std::string convert_ground(std::set<material_handle> const &Materials);
struct ground_conversion
{
bool open{false};
std::map<material_handle, std::size_t> materials;
std::set<material_handle> chosen;
std::string status;
} m_ground_conversion;
void render_ground_conversion();
// handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk)
void handle_chunk_edit_click(bool DeleteMode);
// commits authored terrain to disk, enables streaming, and exports the scenery
@@ -234,6 +247,8 @@ class editor_mode : public application_mode, private editor_track::observer
// grid-aligned manual chunks, keyed by (cx,cz) on the global chunk grid
std::map<std::pair<int, int>, std::unique_ptr<editor_terrain>> m_grid_chunks;
bool m_terrain_sculpt{false}; // when true, LMB sculpts terrain instead of picking
bool m_terrain_brush_smooth{false}; // the brush evens the ground out instead of raising or lowering it
bool m_terrain_tab_wanted{false}; // the settings window brings the terrain tab forward the next time it's drawn
bool m_chunk_edit{false}; // when true, LMB adds/removes whole chunks
int m_terrain_cells{32}; // grid resolution (quads per side)
int m_terrain_chunks{4}; // chunks per side for a chunked terrain
@@ -655,6 +670,18 @@ class editor_mode : public application_mode, private editor_track::observer
std::vector<profile::issue> issues;
double origin{0.0}; // chainage of the start of the route
double departure{0.0}; // largest difference between the grade line and the track
profile::ground_fit fit;
// the editor terrain led to the grade line: formation under the track, slopes down or up to the ground
struct earthworks
{
double depth{0.7}; // m, formation below the top of the rail
double half_width{3.5}; // m, from the axis of the route to the edge of the formation
double slope{1.5}; // horizontal run of the slopes per metre of height
double reach{60.0}; // m, from the edge of the formation, where the slopes end even if they don't get to the ground
double rounding{1.5}; // m, over which the slopes bend into the formation and into the ground
// heights of the terrain before the last shaping
std::vector<std::pair<editor_terrain *, std::vector<float>>> undo;
} earthworks;
double view_from{0.0};
double view_to{100.0};
double view_centre{0.0};
@@ -708,6 +735,10 @@ class editor_mode : public application_mode, private editor_track::observer
void profile_take(editor_track::route Route, TTrack *From, TTrack *To);
void profile_resample();
void profile_recognize();
void profile_fit_ground();
void profile_shape_ground();
void profile_restore_ground();
void render_profile_earthworks();
void profile_check();
void profile_apply();
void profile_fit_view();

View File

@@ -24,6 +24,7 @@ http://mozilla.org/MPL/2.0/.
#include <cmath>
#include <cstdio>
#include <limits>
#include <map>
#include <optional>
#include <sstream>
@@ -442,6 +443,262 @@ void editor_mode::profile_recognize()
state.status = format(STR_C("Grade line recognized from the track: %zu points, largest departure %.3f m"), state.line.points.size(), state.departure);
}
void editor_mode::profile_fit_ground()
{
auto &state{m_profile};
std::vector<double> chainages;
for (auto const &sample : state.samples)
chainages.push_back(sample.chainage);
if (std::none_of(state.terrain.begin(), state.terrain.end(), [](double const Height) { return std::isfinite(Height); }))
{
state.error = STR_C("There's no ground under the route");
return;
}
auto settings{state.line};
settings.speed = route_speed(state.route);
auto line{profile::fit_ground(chainages, state.terrain, state.context, settings, state.fit)};
if (line.points.size() < 2)
return;
state.line = std::move(line);
state.selected = -1;
state.error.clear();
profile_check();
double fill{0.0}, cut{0.0};
for (std::size_t i = 0; i < state.samples.size(); ++i)
{
if (false == std::isfinite(state.terrain[i]))
continue;
auto const difference{profile::elevation(state.line, state.samples[i].chainage) - state.terrain[i]};
fill = std::max(fill, difference);
cut = std::max(cut, -difference);
}
state.status = format(STR_C("Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m"), state.line.points.size(), fill, cut);
}
void editor_mode::profile_shape_ground()
{
auto &state{m_profile};
auto &works{state.earthworks};
auto const &samples{state.samples};
if (samples.size() < 2 || state.line.points.size() < 2)
return;
auto const slope{std::max(0.1, works.slope)};
auto const reach{works.half_width + works.reach};
auto const terrains{active_terrains()};
if (terrains.empty())
{
state.error = STR_C("There's no editor terrain to shape: convert the scenery ground first");
return;
}
// the pieces of the axis of the route, found by the cell of a grid over the plan
double constexpr cell{16.0};
auto const key = [&](double const X, double const Z) { return std::make_pair(static_cast<int>(std::floor(X / cell)), static_cast<int>(std::floor(Z / cell))); };
std::map<std::pair<int, int>, std::vector<std::size_t>> grid;
glm::dvec2 low{std::numeric_limits<double>::max()}, high{-std::numeric_limits<double>::max()};
for (std::size_t i = 0; i + 1 < samples.size(); ++i)
{
auto const &a{samples[i].position};
auto const &b{samples[i + 1].position};
auto const from{key(std::min(a.x, b.x), std::min(a.z, b.z))};
auto const to{key(std::max(a.x, b.x), std::max(a.z, b.z))};
for (auto x = from.first; x <= to.first; ++x)
for (auto z = from.second; z <= to.second; ++z)
grid[{x, z}].push_back(i);
low = glm::min(low, glm::dvec2{std::min(a.x, b.x), std::min(a.z, b.z)});
high = glm::max(high, glm::dvec2{std::max(a.x, b.x), std::max(a.z, b.z)});
}
works.undo.clear();
std::size_t moved{0};
double fill{0.0}, cut{0.0};
for (auto *terrain : terrains)
{
auto const before{terrain->heights()};
auto const shaped{terrain->reshape([&](double const X, double const Z, float &Height) {
if (X < low.x - reach || X > high.x + reach || Z < low.y - reach || Z > high.y + reach)
return false;
double distance{reach};
double chainage{-1.0};
auto const from{key(X - reach, Z - reach)};
auto const to{key(X + reach, Z + reach)};
for (auto x = from.first; x <= to.first; ++x)
for (auto z = from.second; z <= to.second; ++z)
{
auto const found{grid.find({x, z})};
if (found == grid.end())
continue;
for (auto const i : found->second)
{
glm::dvec2 const a{samples[i].position.x, samples[i].position.z};
glm::dvec2 const b{samples[i + 1].position.x, samples[i + 1].position.z};
auto const run{b - a};
auto const length{glm::dot(run, run)};
auto const t{length > 1e-12 ? std::clamp(glm::dot(glm::dvec2{X, Z} - a, run) / length, 0.0, 1.0) : 0.0};
auto const offset{glm::length(glm::dvec2{X, Z} - (a + run * t))};
if (offset < distance)
{
distance = offset;
chainage = samples[i].chainage + (samples[i + 1].chainage - samples[i].chainage) * t;
}
}
}
if (chainage < 0.0)
return false;
auto const formation{profile::elevation(state.line, chainage) - works.depth};
auto const ground{static_cast<double>(Height)};
// the bends at the edge of the formation and at the foot of the slope are rounded off over the given length
auto const rounding{std::max(0.0, works.rounding)};
auto const soft_max = [](double const A, double const B, double const K) {
if (K <= 1e-6)
return std::max(A, B);
auto const h{std::max(K - std::abs(A - B), 0.0) / K};
return std::max(A, B) + h * h * K * 0.25;
};
auto const beyond{soft_max(0.0, distance - works.half_width, rounding)};
auto const bend{rounding / slope};
auto const target{ground > formation ? -soft_max(-ground, -(formation + beyond / slope), bend) : soft_max(ground, formation - beyond / slope, bend)};
if (std::abs(target - ground) < 0.005)
return false;
fill = std::max(fill, target - ground);
cut = std::max(cut, ground - target);
Height = static_cast<float>(target);
++moved;
return true;
})};
if (shaped)
works.undo.emplace_back(terrain, before);
}
if (moved == 0)
{
state.status = STR_C("The editor terrain doesn't reach the route, or it's shaped already");
return;
}
state.error.clear();
profile_resample();
state.status = format(STR_C("Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"), moved, fill, cut);
WriteLog("Editor: vertical profile - " + state.status, logtype::generic);
}
void editor_mode::profile_restore_ground()
{
auto &state{m_profile};
auto const terrains{active_terrains()};
std::size_t restored{0};
for (auto const &entry : state.earthworks.undo)
if (std::find(terrains.begin(), terrains.end(), entry.first) != terrains.end())
{
entry.first->restore(entry.second);
++restored;
}
state.earthworks.undo.clear();
profile_resample();
state.status = restored > 0 ? STR_C("The terrain is back as it was before the shaping") : STR_C("The shaped terrain isn't loaded any more");
}
void editor_mode::render_ground_conversion()
{
auto &conversion{m_ground_conversion};
if (false == conversion.open)
{
if (ImGui::Button(STR_C("Convert the scenery ground to editor terrain...")))
{
conversion.open = true;
conversion.materials = ground_materials();
conversion.chosen.clear();
// the material with the most triangles is the ground, most likely
auto const most{std::max_element(conversion.materials.begin(), conversion.materials.end(), [](auto const &A, auto const &B) { return A.second < B.second; })};
if (most != conversion.materials.end())
conversion.chosen.insert(most->first);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the chosen materials with editor terrain of the same shape,\nwhich can be shaped and sculpted; the scenery files lose these triangles on save"));
if (false == conversion.status.empty())
ImGui::TextWrapped("%s", conversion.status.c_str());
return;
}
ImGui::TextWrapped("%s", STR_C("Materials of the scenery triangles: tick the ones which are the ground"));
if (conversion.materials.empty())
ImGui::TextDisabled("%s", STR_C("The scenery has no triangles"));
for (auto const &entry : conversion.materials)
{
auto const *material{GfxRenderer->Material(entry.first)};
auto const name{material != nullptr ? material->GetName() : std::string{"?"}};
bool chosen{conversion.chosen.count(entry.first) != 0};
ImGui::PushID(static_cast<int>(entry.first));
if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), name.c_str(), entry.second).c_str(), &chosen))
{
if (chosen)
conversion.chosen.insert(entry.first);
else
conversion.chosen.erase(entry.first);
}
ImGui::PopID();
}
ImGui::TextWrapped("%s", STR_C("The terrain gets a single texture: that of the material which covers the most. Until saved, loading the scenery again takes it all back."));
if (conversion.chosen.empty())
ImGui::TextDisabled("%s", STR_C("Tick at least one material"));
else if (ImGui::Button(STR_C("Convert")))
{
conversion.status = convert_ground(conversion.chosen);
conversion.open = false;
profile_resample();
}
ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel")))
conversion.open = false;
}
void editor_mode::render_profile_earthworks()
{
auto &state{m_profile};
auto &works{state.earthworks};
if (false == ImGui::TreeNode(STR_C("Shape the terrain to the grade line")))
return;
ImGui::TextWrapped("%s", STR_C("Only the editor terrain changes: a formation under the track and slopes to the ground; the triangles and models of the scenery stay as they are."));
render_ground_conversion();
auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); };
ImGui::PushItemWidth(90.0f);
drag("Formation below the rail top (m)", works.depth, 0.01f, "%.2f");
drag("Half width of the formation (m)", works.half_width, 0.05f, "%.2f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them"));
drag("Slopes 1 : n, n", works.slope, 0.05f, "%.2f");
drag("Slopes reach at most (m)", works.reach, 1.0f, "%.0f");
drag("Rounding of the edges (m)", works.rounding, 0.1f, "%.1f");
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Length over which the slope bends into the formation and into the ground; 0: sharp edges"));
ImGui::PopItemWidth();
works.rounding = std::clamp(works.rounding, 0.0, 20.0);
works.depth = std::clamp(works.depth, 0.0, 5.0);
works.half_width = std::clamp(works.half_width, 0.5, 30.0);
works.slope = std::clamp(works.slope, 0.1, 10.0);
works.reach = std::clamp(works.reach, 1.0, 200.0);
if (has_errors(state.issues))
ImGui::TextDisabled("%s", STR_C("Correct the errors of the grade line first"));
else if (ImGui::Button(STR_C("Shape the terrain")))
profile_shape_ground();
if (false == works.undo.empty())
{
ImGui::SameLine();
if (ImGui::Button(STR_C("Restore the terrain")))
profile_restore_ground();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Puts back the terrain as it was before the last shaping"));
}
if (false == active_terrains().empty())
{
if (ImGui::Button(STR_C("Sculpting...")))
{
set_settings_open(true);
m_terrain_tab_wanted = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Opens the terrain tab of the editor settings: raising, lowering and smoothing with a brush"));
}
ImGui::TreePop();
}
void editor_mode::profile_check()
{
auto &state{m_profile};
@@ -748,6 +1005,20 @@ bool editor_mode::render_profile_parameters()
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Sets the points of the grade line from the existing track"));
ImGui::SameLine();
if (ImGui::Button(STR_C("Fit to the ground")))
{
profile_fit_ground();
state.changed = true;
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Places the breaks of the grade line along the ground under the route,\nwithin the limits and with the least earthworks; the ground itself stays as it is"));
ImGui::PushItemWidth(70.0f);
ImGui::DragScalar(STR_C("m off the ground##fittolerance"), ImGuiDataType_Double, &state.fit.tolerance, 0.05f, nullptr, nullptr, "%.2f");
ImGui::PopItemWidth();
state.fit.tolerance = std::clamp(state.fit.tolerance, 0.05, 50.0);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings"));
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.68f, 0.3f, 1.0f));
if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 0.0f)))
@@ -755,6 +1026,8 @@ bool editor_mode::render_profile_parameters()
ImGui::PopStyleColor(2);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Sets the heights of the track along the route to the grade line, Ctrl+Z takes it back"));
ImGui::Separator();
render_profile_earthworks();
return changed;
}