mirror of
https://github.com/MaSzyna-EU07/maszyna.git
synced 2026-10-29 01:07:28 +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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -385,6 +385,255 @@ line recognize(std::vector<double> const &Chainages, std::vector<double> const &
|
||||
return result;
|
||||
}
|
||||
|
||||
line fit_ground(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, line const &Settings, ground_fit const &Options)
|
||||
{
|
||||
line result;
|
||||
result.speed = Settings.speed;
|
||||
result.norms = Settings.norms;
|
||||
result.join_start = Settings.join_start;
|
||||
result.join_end = Settings.join_end;
|
||||
auto const count{std::min(Chainages.size(), Ground.size())};
|
||||
if (count < 2)
|
||||
return result;
|
||||
|
||||
// gaps in the ground bridged straight, past the outermost samples it stays level
|
||||
std::vector<double> ground(Ground.begin(), Ground.begin() + count);
|
||||
std::vector<std::size_t> known;
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (std::isfinite(ground[i]))
|
||||
known.push_back(i);
|
||||
if (known.empty())
|
||||
return result;
|
||||
for (std::size_t i = 0, k = 0; i < count; ++i)
|
||||
{
|
||||
if (std::isfinite(ground[i]))
|
||||
continue;
|
||||
while (k + 1 < known.size() && known[k + 1] < i)
|
||||
++k;
|
||||
if (i < known.front())
|
||||
ground[i] = Ground[known.front()];
|
||||
else if (k + 1 >= known.size())
|
||||
ground[i] = Ground[known.back()];
|
||||
else
|
||||
{
|
||||
auto const a{known[k]}, b{known[k + 1]};
|
||||
ground[i] = Ground[a] + (Ground[b] - Ground[a]) * (Chainages[i] - Chainages[a]) / std::max(1e-9, Chainages[b] - Chainages[a]);
|
||||
}
|
||||
}
|
||||
if (Options.smoothing > 1.0)
|
||||
{
|
||||
std::vector<double> sums(count + 1, 0.0);
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
sums[i + 1] = sums[i] + ground[i];
|
||||
std::vector<double> smooth(count);
|
||||
std::size_t low{0}, high{0};
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
{
|
||||
while (Chainages[i] - Chainages[low] > Options.smoothing * 0.5)
|
||||
++low;
|
||||
while (high + 1 < count && Chainages[high + 1] - Chainages[i] <= Options.smoothing * 0.5)
|
||||
++high;
|
||||
high = std::max(high, i);
|
||||
smooth[i] = (sums[high + 1] - sums[low]) / static_cast<double>(high - low + 1);
|
||||
}
|
||||
ground.swap(smooth);
|
||||
}
|
||||
|
||||
bool const start_fixed{Settings.join_start && Context.start_joined};
|
||||
bool const end_fixed{Settings.join_end && Context.end_joined};
|
||||
|
||||
// first guess of the points: the ground simplified to within the tolerance
|
||||
std::vector<std::size_t> knots{0, count - 1};
|
||||
{
|
||||
std::vector<char> keep(count, 0);
|
||||
keep.front() = keep.back() = 1;
|
||||
std::vector<std::pair<std::size_t, std::size_t>> stack{{0, count - 1}};
|
||||
while (false == stack.empty())
|
||||
{
|
||||
auto const [a, b]{stack.back()};
|
||||
stack.pop_back();
|
||||
if (b <= a + 1)
|
||||
continue;
|
||||
auto const run{std::max(1e-9, Chainages[b] - Chainages[a])};
|
||||
double worst{0.0};
|
||||
std::size_t index{a};
|
||||
for (auto i = a + 1; i < b; ++i)
|
||||
{
|
||||
auto const chord{ground[a] + (ground[b] - ground[a]) * (Chainages[i] - Chainages[a]) / run};
|
||||
auto const departure{std::abs(ground[i] - chord)};
|
||||
if (departure > worst)
|
||||
{
|
||||
worst = departure;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (worst > Options.tolerance)
|
||||
{
|
||||
keep[index] = 1;
|
||||
stack.push_back({a, index});
|
||||
stack.push_back({index, b});
|
||||
}
|
||||
}
|
||||
knots.clear();
|
||||
for (std::size_t i = 0; i < count; ++i)
|
||||
if (keep[i] != 0)
|
||||
knots.push_back(i);
|
||||
}
|
||||
|
||||
auto const segment_limit = [&](double const From, double const To) {
|
||||
auto limit{result.norms.grade_max};
|
||||
for (auto const &zone : Context.switches)
|
||||
if (overlaps(From, To, zone))
|
||||
limit = std::min(limit, result.norms.grade_switch_max);
|
||||
return limit / 1000.0;
|
||||
};
|
||||
// elevations of the points with the least squares of the departures from the ground
|
||||
auto const solve = [&](std::vector<std::size_t> const &Knots) {
|
||||
auto const n{Knots.size()};
|
||||
std::vector<double> lower(n, 0.0), diagonal(n, 1e-9), upper(n, 0.0), right(n, 0.0);
|
||||
auto const add = [&](std::size_t const Interval, double const T, double const Value, double const Weight) {
|
||||
diagonal[Interval] += Weight * (1.0 - T) * (1.0 - T);
|
||||
diagonal[Interval + 1] += Weight * T * T;
|
||||
upper[Interval] += Weight * (1.0 - T) * T;
|
||||
lower[Interval + 1] += Weight * (1.0 - T) * T;
|
||||
right[Interval] += Weight * (1.0 - T) * Value;
|
||||
right[Interval + 1] += Weight * T * Value;
|
||||
};
|
||||
auto const interval_of = [&](double const Chainage) {
|
||||
std::size_t k{0};
|
||||
while (k + 2 < n && Chainage >= Chainages[Knots[k + 1]])
|
||||
++k;
|
||||
return k;
|
||||
};
|
||||
for (std::size_t k = 0; k + 1 < n; ++k)
|
||||
{
|
||||
auto const from{Chainages[Knots[k]]};
|
||||
auto const run{std::max(1e-9, Chainages[Knots[k + 1]] - from)};
|
||||
auto const last{k + 2 == n ? Knots[k + 1] : Knots[k + 1] - 1};
|
||||
for (auto i = Knots[k]; i <= last; ++i)
|
||||
add(k, (Chainages[i] - from) / run, ground[i], 1.0);
|
||||
}
|
||||
for (auto const &fixed : Context.fixed)
|
||||
{
|
||||
auto const k{interval_of(fixed.first)};
|
||||
auto const from{Chainages[Knots[k]]};
|
||||
auto const t{std::clamp((fixed.first - from) / std::max(1e-9, Chainages[Knots[k + 1]] - from), 0.0, 1.0)};
|
||||
add(k, t, fixed.second, 1e6);
|
||||
}
|
||||
auto const pin = [&](std::size_t const Index, double const Value) {
|
||||
lower[Index] = upper[Index] = 0.0;
|
||||
diagonal[Index] = 1.0;
|
||||
right[Index] = Value;
|
||||
};
|
||||
if (start_fixed)
|
||||
pin(0, Context.start_elevation);
|
||||
if (end_fixed)
|
||||
pin(n - 1, Context.end_elevation);
|
||||
for (std::size_t k = 1; k < n; ++k)
|
||||
{
|
||||
auto const factor{lower[k] / diagonal[k - 1]};
|
||||
diagonal[k] -= factor * upper[k - 1];
|
||||
right[k] -= factor * right[k - 1];
|
||||
}
|
||||
std::vector<double> z(n);
|
||||
z[n - 1] = right[n - 1] / diagonal[n - 1];
|
||||
for (auto k = n - 1; k-- > 0;)
|
||||
z[k] = (right[k] - upper[k] * z[k + 1]) / diagonal[k];
|
||||
|
||||
// grades kept within the limits, the fixed ends don't move
|
||||
for (int pass = 0; pass < 4; ++pass)
|
||||
{
|
||||
for (std::size_t k = 0; k + 1 < n; ++k)
|
||||
{
|
||||
if (k + 2 == n && end_fixed)
|
||||
continue;
|
||||
auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]};
|
||||
auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run};
|
||||
z[k + 1] = std::clamp(z[k + 1], z[k] - step, z[k] + step);
|
||||
}
|
||||
for (auto k = n - 1; k-- > 0;)
|
||||
{
|
||||
if (k == 0 && start_fixed)
|
||||
continue;
|
||||
auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]};
|
||||
auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run};
|
||||
z[k] = std::clamp(z[k], z[k + 1] - step, z[k + 1] + step);
|
||||
}
|
||||
}
|
||||
return z;
|
||||
};
|
||||
|
||||
auto const radius{std::ceil(required_radius(result) / 10.0) * 10.0};
|
||||
auto const threshold{result.norms.curve_threshold / 1000.0};
|
||||
std::vector<double> z;
|
||||
for (int iteration = 0; iteration < 1000; ++iteration)
|
||||
{
|
||||
z = solve(knots);
|
||||
auto const n{knots.size()};
|
||||
if (n <= 2)
|
||||
break;
|
||||
auto const chainage = [&](std::size_t const K) { return Chainages[knots[K]]; };
|
||||
auto const slope = [&](std::size_t const K) { return (z[K + 1] - z[K]) / std::max(1e-9, chainage(K + 1) - chainage(K)); };
|
||||
std::vector<double> change(n, 0.0), tangent(n, 0.0);
|
||||
for (std::size_t k = 1; k + 1 < n; ++k)
|
||||
{
|
||||
change[k] = std::abs(slope(k) - slope(k - 1));
|
||||
tangent[k] = change[k] > threshold + 1e-9 ? radius * change[k] * 0.5 : 0.0;
|
||||
}
|
||||
// the curve of a joint starts at the end of the route and takes its whole length
|
||||
if (start_fixed)
|
||||
{
|
||||
change[0] = std::abs(slope(0) - Context.start_grade);
|
||||
tangent[0] = change[0] > threshold + 1e-9 ? radius * change[0] : 0.0;
|
||||
}
|
||||
if (end_fixed)
|
||||
{
|
||||
change[n - 1] = std::abs(slope(n - 2) - Context.end_grade);
|
||||
tangent[n - 1] = change[n - 1] > threshold + 1e-9 ? radius * change[n - 1] : 0.0;
|
||||
}
|
||||
|
||||
std::size_t removed{0};
|
||||
for (std::size_t k = 1; k + 1 < n && removed == 0; ++k)
|
||||
for (auto const &zone : Context.switches)
|
||||
if (overlaps(chainage(k) - tangent[k] - 1.0, chainage(k) + tangent[k] + 1.0, zone))
|
||||
{
|
||||
removed = k;
|
||||
break;
|
||||
}
|
||||
if (removed == 0)
|
||||
{
|
||||
double worst{0.0};
|
||||
for (std::size_t k = 0; k + 1 < n; ++k)
|
||||
{
|
||||
bool const inner{(k > 0 || tangent[0] > 0.0) && (k + 2 < n || tangent[n - 1] > 0.0)};
|
||||
auto const needed{tangent[k] + tangent[k + 1] + (inner ? result.norms.element_min : 0.0)};
|
||||
auto const shortage{needed - (chainage(k + 1) - chainage(k))};
|
||||
if (shortage <= 1e-6 || shortage <= worst)
|
||||
continue;
|
||||
worst = shortage;
|
||||
if (k == 0)
|
||||
removed = 1;
|
||||
else if (k + 2 == n)
|
||||
removed = n - 2;
|
||||
else
|
||||
removed = change[k] <= change[k + 1] ? k : k + 1;
|
||||
}
|
||||
}
|
||||
if (removed == 0)
|
||||
break;
|
||||
knots.erase(knots.begin() + removed);
|
||||
}
|
||||
|
||||
for (std::size_t k = 0; k < knots.size(); ++k)
|
||||
{
|
||||
pvi point;
|
||||
point.chainage = Chainages[knots[k]];
|
||||
point.elevation = z[k];
|
||||
result.points.push_back(point);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<double> breaks(line const &Line)
|
||||
{
|
||||
std::vector<double> result;
|
||||
|
||||
@@ -111,6 +111,15 @@ struct recognition
|
||||
// grade line of existing track, from its elevations and grades sampled along the chainage
|
||||
line recognize(std::vector<double> const &Chainages, std::vector<double> const &Elevations, std::vector<double> const &Grades, recognition const &Options);
|
||||
|
||||
struct ground_fit
|
||||
{
|
||||
double tolerance{1.0}; // m, departure from the ground which doesn't yet need a change of grade
|
||||
double smoothing{20.0}; // m, length over which the ground is averaged
|
||||
};
|
||||
// grade line which follows the ground within the limits of Settings, with the least earthworks.
|
||||
// Ground is NaN where there's none; the joined ends of the route keep the elevation of the adjoining track
|
||||
line fit_ground(std::vector<double> const &Chainages, std::vector<double> const &Ground, context const &Context, line const &Settings, ground_fit const &Options);
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -154,28 +154,72 @@ bool editor_terrain::block_flat(int X0, int Z0, int X1, int Z1, float Error) con
|
||||
return true;
|
||||
}
|
||||
|
||||
// adaptive quadtree: collapse flat blocks into a single quad, otherwise split into four
|
||||
void editor_terrain::emit_block(int X0, int Z0, int X1, int Z1, float Error, std::vector<world_vertex> &Out) const
|
||||
// adaptive quadtree: flat blocks stay whole, the others are split into four
|
||||
void editor_terrain::collect_blocks(int X0, int Z0, int X1, int Z1, float Error, std::vector<std::array<int, 4>> &Out) const
|
||||
{
|
||||
bool const splitx = (X1 - X0) > 1;
|
||||
bool const splitz = (Z1 - Z0) > 1;
|
||||
|
||||
if ((!splitx && !splitz) || block_flat(X0, Z0, X1, Z1, Error))
|
||||
{
|
||||
emit_quad(X0, Z0, X1, Z1, Out);
|
||||
Out.push_back({X0, Z0, X1, Z1});
|
||||
return;
|
||||
}
|
||||
|
||||
int const xm = splitx ? (X0 + X1) / 2 : X1;
|
||||
int const zm = splitz ? (Z0 + Z1) / 2 : Z1;
|
||||
|
||||
emit_block(X0, Z0, xm, zm, Error, Out);
|
||||
collect_blocks(X0, Z0, xm, zm, Error, Out);
|
||||
if (splitx)
|
||||
emit_block(xm, Z0, X1, zm, Error, Out);
|
||||
collect_blocks(xm, Z0, X1, zm, Error, Out);
|
||||
if (splitz)
|
||||
emit_block(X0, zm, xm, Z1, Error, Out);
|
||||
collect_blocks(X0, zm, xm, Z1, Error, Out);
|
||||
if (splitx && splitz)
|
||||
emit_block(xm, zm, X1, Z1, Error, Out);
|
||||
collect_blocks(xm, zm, X1, Z1, Error, Out);
|
||||
}
|
||||
|
||||
void editor_terrain::emit_block(std::array<int, 4> const &Block, std::vector<char> const &Marked, std::vector<world_vertex> &Out) const
|
||||
{
|
||||
int const x0 = Block[0], z0 = Block[1], x1 = Block[2], z1 = Block[3];
|
||||
// the edges walked the way the quads wind
|
||||
std::vector<std::pair<int, int>> ring;
|
||||
for (int z = z0; z < z1; ++z)
|
||||
if (z == z0 || Marked[index(x0, z)] != 0)
|
||||
ring.push_back({x0, z});
|
||||
for (int x = x0; x < x1; ++x)
|
||||
if (x == x0 || Marked[index(x, z1)] != 0)
|
||||
ring.push_back({x, z1});
|
||||
for (int z = z1; z > z0; --z)
|
||||
if (z == z1 || Marked[index(x1, z)] != 0)
|
||||
ring.push_back({x1, z});
|
||||
for (int x = x1; x > x0; --x)
|
||||
if (x == x1 || Marked[index(x, z0)] != 0)
|
||||
ring.push_back({x, z0});
|
||||
if (ring.size() <= 4)
|
||||
{
|
||||
emit_quad(x0, z0, x1, z1, Out);
|
||||
return;
|
||||
}
|
||||
|
||||
world_vertex centre;
|
||||
if ((x1 - x0) % 2 == 0 && (z1 - z0) % 2 == 0)
|
||||
centre = make_vertex((x0 + x1) / 2, (z0 + z1) / 2);
|
||||
else
|
||||
{
|
||||
auto const a = make_vertex(x0, z0), b = make_vertex(x1, z0), c = make_vertex(x0, z1), d = make_vertex(x1, z1);
|
||||
centre = a;
|
||||
centre.position = (a.position + b.position + c.position + d.position) * 0.25;
|
||||
centre.normal = glm::normalize(a.normal + b.normal + c.normal + d.normal);
|
||||
centre.texture = (a.texture + b.texture + c.texture + d.texture) * 0.25f;
|
||||
}
|
||||
for (std::size_t i = 0; i < ring.size(); ++i)
|
||||
{
|
||||
auto const &from = ring[i];
|
||||
auto const &to = ring[(i + 1) % ring.size()];
|
||||
Out.push_back(centre);
|
||||
Out.push_back(make_vertex(from.first, from.second));
|
||||
Out.push_back(make_vertex(to.first, to.second));
|
||||
}
|
||||
}
|
||||
|
||||
void editor_terrain::build_vertices(std::vector<world_vertex> &Out, bool Simplify) const
|
||||
@@ -185,7 +229,16 @@ void editor_terrain::build_vertices(std::vector<world_vertex> &Out, bool Simplif
|
||||
|
||||
if (Simplify)
|
||||
{
|
||||
emit_block(0, 0, m_cells, m_cells, m_simplify_error, Out);
|
||||
std::vector<std::array<int, 4>> blocks;
|
||||
collect_blocks(0, 0, m_cells, m_cells, m_simplify_error, blocks);
|
||||
// the corners of the blocks, and the whole edge of the patch which the neighbouring patch meets in its own way
|
||||
std::vector<char> marked(static_cast<std::size_t>(m_cells + 1) * (m_cells + 1), 0);
|
||||
for (int i = 0; i <= m_cells; ++i)
|
||||
marked[index(i, 0)] = marked[index(i, m_cells)] = marked[index(0, i)] = marked[index(m_cells, i)] = 1;
|
||||
for (auto const &block : blocks)
|
||||
marked[index(block[0], block[1])] = marked[index(block[2], block[1])] = marked[index(block[0], block[3])] = marked[index(block[2], block[3])] = 1;
|
||||
for (auto const &block : blocks)
|
||||
emit_block(block, marked, Out);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,6 +331,12 @@ bool editor_terrain::contains(double X, double Z) const
|
||||
|
||||
double editor_terrain::height_at(double X, double Z) const
|
||||
{
|
||||
return height_in(m_heights, X, Z);
|
||||
}
|
||||
|
||||
double editor_terrain::height_in(std::vector<float> const &Heights, double X, double Z) const
|
||||
{
|
||||
auto const &m_heights = Heights;
|
||||
double const fx = (X - m_x0) / m_cellsize;
|
||||
double const fz = (Z - m_z0) / m_cellsize;
|
||||
int ix = static_cast<int>(std::floor(fx));
|
||||
@@ -330,6 +389,62 @@ bool editor_terrain::sculpt(double X, double Z, double Radius, double Strength)
|
||||
return changed;
|
||||
}
|
||||
|
||||
bool editor_terrain::smooth(double X, double Z, double Radius, double Amount, std::function<double(double X, double Z, double Fallback)> const &Ground)
|
||||
{
|
||||
if (!valid() || Radius <= 0.0 || Amount <= 0.0)
|
||||
return false;
|
||||
double const reach = Radius + m_cellsize;
|
||||
if (X + reach < m_x0 || X - reach > m_x0 + m_cells * m_cellsize || Z + reach < m_z0 || Z - reach > m_z0 + m_cells * m_cellsize)
|
||||
return false;
|
||||
|
||||
double const step = m_cellsize;
|
||||
return reshape([&](double const Vx, double const Vz, float &Height) {
|
||||
double const d = std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z));
|
||||
if (d > Radius)
|
||||
return false;
|
||||
double const own = Height;
|
||||
// the neighbours a cell away, read from the ground as it was before the stroke, whichever patch holds them
|
||||
double const average = (Ground(Vx - step, Vz, own) + Ground(Vx + step, Vz, own) + Ground(Vx, Vz - step, own) + Ground(Vx, Vz + step, own) + own * 4.0) / 8.0;
|
||||
double const falloff = 0.5 * (std::cos(kPi * d / Radius) + 1.0);
|
||||
double const smoothed = own + (average - own) * std::min(1.0, Amount * falloff);
|
||||
if (std::abs(smoothed - own) < 1e-5)
|
||||
return false;
|
||||
Height = static_cast<float>(smoothed);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool editor_terrain::reshape(std::function<bool(double X, double Z, float &Height)> const &Shaper)
|
||||
{
|
||||
if (!valid())
|
||||
return false;
|
||||
|
||||
bool changed = false;
|
||||
for (int iz = 0; iz <= m_cells; ++iz)
|
||||
for (int ix = 0; ix <= m_cells; ++ix)
|
||||
changed |= Shaper(m_x0 + static_cast<double>(ix) * m_cellsize, m_z0 + static_cast<double>(iz) * m_cellsize, m_heights[index(ix, iz)]);
|
||||
|
||||
if (changed)
|
||||
{
|
||||
m_simplify = false;
|
||||
m_dirty = true;
|
||||
m_modified = true;
|
||||
regenerate(false);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void editor_terrain::restore(std::vector<float> const &Heights)
|
||||
{
|
||||
if (!valid() || Heights.size() != m_heights.size())
|
||||
return;
|
||||
m_heights = Heights;
|
||||
m_simplify = false;
|
||||
m_dirty = true;
|
||||
m_modified = true;
|
||||
regenerate(false);
|
||||
}
|
||||
|
||||
glm::dvec3 editor_terrain::centre() const
|
||||
{
|
||||
double const c = 0.5 * static_cast<double>(m_cells) * m_cellsize;
|
||||
|
||||
@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
@@ -44,10 +45,20 @@ class editor_terrain
|
||||
bool contains(double X, double Z) const;
|
||||
// surface height at (X,Z) (bilinear over the covering quad); only valid when contains() is true
|
||||
double height_at(double X, double Z) const;
|
||||
// the same, from heights taken from heights() earlier
|
||||
double height_in(std::vector<float> const &Heights, double X, double Z) const;
|
||||
|
||||
// raises/lowers vertices within Radius of (X,Z) by Strength (metres, signed), with a smooth
|
||||
// falloff; regenerates the rendered geometry (full resolution). returns true if anything changed.
|
||||
bool sculpt(double X, double Z, double Radius, double Strength);
|
||||
// gives every grid vertex to Shaper, with its world position and height; Shaper returns true if it changed the height.
|
||||
// regenerates the rendered geometry (full resolution). returns true if anything changed.
|
||||
bool reshape(std::function<bool(double X, double Z, float &Height)> const &Shaper);
|
||||
// pulls the vertices within Radius of (X,Z) towards the average of their neighbours, Amount (0..1) of the way at the centre,
|
||||
// with a smooth falloff. Ground gives the height before the stroke at a world point, Fallback where there's no terrain
|
||||
bool smooth(double X, double Z, double Radius, double Amount, std::function<double(double X, double Z, double Fallback)> const &Ground);
|
||||
// puts back heights taken from heights() earlier
|
||||
void restore(std::vector<float> const &Heights);
|
||||
|
||||
// rebuilds the rendered mesh, collapsing regions flatter than ErrorMetres into larger quads
|
||||
// (adaptive quadtree). the editable heightmap is untouched, so sculpting/raycast stay exact.
|
||||
@@ -63,6 +74,7 @@ class editor_terrain
|
||||
glm::dvec3 centre() const;
|
||||
float extent() const { return m_cells * m_cellsize; }
|
||||
bool valid() const { return m_cells > 0; }
|
||||
gfx::geometry_handle geometry() const { return m_geometry; }
|
||||
bool optimized() const { return m_simplify; }
|
||||
// set by sculpt() when the mesh changed and is currently full-resolution; cleared by optimize().
|
||||
// used to auto-simplify only the chunks that were actually edited, once a stroke finishes.
|
||||
@@ -88,7 +100,9 @@ class editor_terrain
|
||||
void build_vertices(std::vector<world_vertex> &Out, bool Simplify) const;
|
||||
// adaptive quadtree helpers (used when Simplify is on)
|
||||
bool block_flat(int X0, int Z0, int X1, int Z1, float Error) const;
|
||||
void emit_block(int X0, int Z0, int X1, int Z1, float Error, std::vector<world_vertex> &Out) const;
|
||||
void collect_blocks(int X0, int Z0, int X1, int Z1, float Error, std::vector<std::array<int, 4>> &Out) const;
|
||||
// a block as a fan through the marked grid vertices of its edges, so it meets its smaller neighbours without cracks
|
||||
void emit_block(std::array<int, 4> const &Block, std::vector<char> const &Marked, std::vector<world_vertex> &Out) const;
|
||||
void emit_quad(int X0, int Z0, int X1, int Z1, std::vector<world_vertex> &Out) const;
|
||||
// rebuilds and re-uploads the rendered geometry (Replace when the count is unchanged, otherwise
|
||||
// a fresh chunk whose handle is swapped into the shape)
|
||||
|
||||
@@ -10,7 +10,10 @@ http://mozilla.org/MPL/2.0/.
|
||||
#include "stdafx.h"
|
||||
#include "editor/editorTerrainStreamer.hpp"
|
||||
|
||||
#include "utilities/Globals.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -133,13 +136,25 @@ void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
return;
|
||||
|
||||
chunk_key const camera = key_at(CameraPos.x, CameraPos.z);
|
||||
// the terrain reaches as far as the rest of the scenery is drawn
|
||||
int const radius = std::max(m_radius, std::min(48, static_cast<int>(std::ceil(Global.BaseDrawRange / std::max(1.0f, chunk_world_size())))));
|
||||
|
||||
// load any missing chunk inside the radius (one build per frame keeps the hitch small)
|
||||
bool built = false;
|
||||
for (int dz = -m_radius; dz <= m_radius && !built; ++dz)
|
||||
for (int dx = -m_radius; dx <= m_radius && !built; ++dx)
|
||||
// missing chunks inside the radius, the nearest first, as many as fit in a few milliseconds of the frame
|
||||
if (m_ring_radius != radius)
|
||||
{
|
||||
m_ring.clear();
|
||||
for (int dz = -radius; dz <= radius; ++dz)
|
||||
for (int dx = -radius; dx <= radius; ++dx)
|
||||
m_ring.push_back({dx, dz});
|
||||
std::sort(m_ring.begin(), m_ring.end(), [](chunk_key const &A, chunk_key const &B) { return A.first * A.first + A.second * A.second < B.first * B.first + B.second * B.second; });
|
||||
m_ring_radius = radius;
|
||||
}
|
||||
auto const started = std::chrono::steady_clock::now();
|
||||
for (auto const &offset : m_ring)
|
||||
{
|
||||
chunk_key const key{camera.first + dx, camera.second + dz};
|
||||
if (std::chrono::steady_clock::now() - started > std::chrono::milliseconds(4))
|
||||
break;
|
||||
chunk_key const key{camera.first + offset.first, camera.second + offset.second};
|
||||
if (m_chunks.count(key))
|
||||
continue;
|
||||
|
||||
@@ -169,7 +184,6 @@ void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
if (m_auto_optimize)
|
||||
terrain->optimize(m_simplify_error);
|
||||
m_chunks.emplace(key, std::move(terrain));
|
||||
built = true; // amortise: at most one new chunk per frame
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +192,7 @@ void terrain_streamer::update(glm::dvec3 const &CameraPos)
|
||||
{
|
||||
int const dx = it->first.first - camera.first;
|
||||
int const dz = it->first.second - camera.second;
|
||||
if (std::abs(dx) > m_radius || std::abs(dz) > m_radius)
|
||||
if (std::abs(dx) > radius || std::abs(dz) > radius)
|
||||
{
|
||||
if (it->second)
|
||||
{
|
||||
|
||||
@@ -77,6 +77,7 @@ class terrain_streamer
|
||||
int cells() const { return m_cells; }
|
||||
float cellsize() const { return m_cellsize; }
|
||||
int radius() const { return m_radius; }
|
||||
std::string const &texture() const { return m_texture; }
|
||||
|
||||
// writes every resident, edited chunk to disk (without unloading) - used on save
|
||||
void flush();
|
||||
@@ -108,6 +109,9 @@ class terrain_streamer
|
||||
|
||||
std::map<chunk_key, std::unique_ptr<editor_terrain>> m_chunks;
|
||||
std::map<chunk_key, std::uint8_t> m_known; // cached per-chunk flag bits
|
||||
// offsets of the chunks around the camera, the nearest first, for the radius in effect
|
||||
std::vector<chunk_key> m_ring;
|
||||
int m_ring_radius{-1};
|
||||
};
|
||||
|
||||
// single, simulation-level streamer instance shared by the editor (authoring) and the scenery
|
||||
|
||||
@@ -1891,3 +1891,135 @@ msgstr "Łuk %d esownicy"
|
||||
msgid "Details"
|
||||
msgstr "Szczegóły"
|
||||
|
||||
|
||||
msgid "There's no ground under the route"
|
||||
msgstr "Pod trasą nie ma terenu"
|
||||
|
||||
msgid "Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m"
|
||||
msgstr "Niweleta dopasowana do terenu: %zu punktów, nasyp do %.2f m, wykop do %.2f m"
|
||||
|
||||
msgid "Fit to the ground"
|
||||
msgstr "Dopasuj do terenu"
|
||||
|
||||
msgid "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"
|
||||
msgstr "Rozmieszcza załamania niwelety wzdłuż terenu pod trasą,\nw granicach norm i z najmniejszymi robotami ziemnymi; sam teren się nie zmienia"
|
||||
|
||||
msgid "m off the ground##fittolerance"
|
||||
msgstr "m od terenu##fittolerance"
|
||||
|
||||
msgid "How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings"
|
||||
msgstr "Jak daleko niweleta może odejść od terenu, zanim się załamie;\nwięcej to mniej załamań, ale wyższe nasypy i głębsze wykopy"
|
||||
|
||||
msgid "There's no editor terrain to shape"
|
||||
msgstr "Nie ma terenu edytora do przekształcenia"
|
||||
|
||||
msgid "The editor terrain doesn't reach the route, or it's shaped already"
|
||||
msgstr "Teren edytora nie sięga trasy albo jest już przekształcony"
|
||||
|
||||
msgid "Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m"
|
||||
msgstr "Teren przekształcony w %zu punktach: nasyp do %.2f m, wykop do %.2f m"
|
||||
|
||||
msgid "The terrain is back as it was before the shaping"
|
||||
msgstr "Teren przywrócony do stanu sprzed przekształcenia"
|
||||
|
||||
msgid "The shaped terrain isn't loaded any more"
|
||||
msgstr "Przekształcony teren nie jest już wczytany"
|
||||
|
||||
msgid "Shape the terrain to the grade line"
|
||||
msgstr "Przekształć teren pod niweletę"
|
||||
|
||||
msgid "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."
|
||||
msgstr "Zmienia się tylko teren edytora: korona podtorza pod torem i skarpy do terenu; trójkąty i modele scenerii zostają bez zmian."
|
||||
|
||||
msgid "Formation below the rail top (m)"
|
||||
msgstr "Korona podtorza pod główką szyny (m)"
|
||||
|
||||
msgid "Half width of the formation (m)"
|
||||
msgstr "Połowa szerokości korony (m)"
|
||||
|
||||
msgid "From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them"
|
||||
msgstr "Od osi trasy do krawędzi korony;\ndla dwóch torów dodaj rozstaw między nimi"
|
||||
|
||||
msgid "Slopes 1 : n, n"
|
||||
msgstr "Skarpy 1 : n, n"
|
||||
|
||||
msgid "Slopes reach at most (m)"
|
||||
msgstr "Skarpy sięgają najwyżej (m)"
|
||||
|
||||
msgid "Correct the errors of the grade line first"
|
||||
msgstr "Najpierw popraw błędy niwelety"
|
||||
|
||||
msgid "Shape the terrain"
|
||||
msgstr "Przekształć teren"
|
||||
|
||||
msgid "Restore the terrain"
|
||||
msgstr "Przywróć teren"
|
||||
|
||||
msgid "Puts back the terrain as it was before the last shaping"
|
||||
msgstr "Przywraca teren sprzed ostatniego przekształcenia"
|
||||
|
||||
msgid "There's no editor terrain to shape: convert the scenery ground first"
|
||||
msgstr "Nie ma terenu edytora do przekształcenia: najpierw zamień grunt scenerii"
|
||||
|
||||
msgid "The chosen materials have no triangles"
|
||||
msgstr "Wybrane materiały nie mają trójkątów"
|
||||
|
||||
msgid "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"
|
||||
msgstr "Grunt zamieniony: %zu chunków terenu edytora, %zu brył zdjętych ze sceny, %zu węzłów trójkątów zniknie z plików scenerii przy zapisie"
|
||||
|
||||
msgid ", %zu chunks kept as there was editor terrain already"
|
||||
msgstr ", %zu chunków pominiętych, bo był tam już teren edytora"
|
||||
|
||||
msgid ", %zu nodes stay as their files can't be rewritten"
|
||||
msgstr ", %zu węzłów zostaje, bo ich plików nie da się zapisać"
|
||||
|
||||
msgid "Convert the scenery ground to editor terrain..."
|
||||
msgstr "Zamień grunt scenerii na teren edytora..."
|
||||
|
||||
msgid "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"
|
||||
msgstr "Zastępuje trójkąty wybranych materiałów terenem edytora o tym samym kształcie,\nktóry można przekształcać i rzeźbić; pliki scenerii tracą te trójkąty przy zapisie"
|
||||
|
||||
msgid "Materials of the scenery triangles: tick the ones which are the ground"
|
||||
msgstr "Materiały trójkątów scenerii: zaznacz te, które są gruntem"
|
||||
|
||||
msgid "The scenery has no triangles"
|
||||
msgstr "Sceneria nie ma trójkątów"
|
||||
|
||||
msgid "%s (%zu triangles)"
|
||||
msgstr "%s (%zu trójkątów)"
|
||||
|
||||
msgid "The terrain gets a single texture: that of the material which covers the most. Until saved, loading the scenery again takes it all back."
|
||||
msgstr "Teren dostaje jedną teksturę: materiału, który pokrywa najwięcej. Do zapisu ponowne wczytanie scenerii cofa wszystko."
|
||||
|
||||
msgid "Tick at least one material"
|
||||
msgstr "Zaznacz co najmniej jeden materiał"
|
||||
|
||||
msgid "Convert"
|
||||
msgstr "Zamień"
|
||||
|
||||
msgid "Sculpt mode (LMB smooth)"
|
||||
msgstr "Rzeźbienie (LPM wygładza)"
|
||||
|
||||
msgid "Sculpt mode (LMB raise / Shift = lower)"
|
||||
msgstr "Rzeźbienie (LPM podnosi / Shift obniża)"
|
||||
|
||||
msgid "Raise / lower"
|
||||
msgstr "Podnoś / obniżaj"
|
||||
|
||||
msgid "Smooth"
|
||||
msgstr "Wygładzaj"
|
||||
|
||||
msgid "Evens out the ground under the brush: bumps and sharp edges go, the shape stays.\nThe strength sets how fast"
|
||||
msgstr "Wyrównuje teren pod pędzlem: znikają nierówności i ostre krawędzie, kształt zostaje.\nSiła ustala, jak szybko"
|
||||
|
||||
msgid "Rounding of the edges (m)"
|
||||
msgstr "Zaokrąglenie krawędzi (m)"
|
||||
|
||||
msgid "Length over which the slope bends into the formation and into the ground; 0: sharp edges"
|
||||
msgstr "Długość, na której skarpa przechodzi w koronę i w teren; 0: ostre krawędzie"
|
||||
|
||||
msgid "Sculpting..."
|
||||
msgstr "Rzeźbienie..."
|
||||
|
||||
msgid "Opens the terrain tab of the editor settings: raising, lowering and smoothing with a brush"
|
||||
msgstr "Otwiera zakładkę terenu w ustawieniach edytora: podnoszenie, obniżanie i wygładzanie pędzlem"
|
||||
|
||||
@@ -129,6 +129,8 @@ void node_layers::clear()
|
||||
m_context = layer_context();
|
||||
m_sources.clear();
|
||||
m_erased.clear();
|
||||
m_shapes.clear();
|
||||
m_shapeserased = false;
|
||||
m_templates.clear();
|
||||
m_instances.clear();
|
||||
m_instance = 0;
|
||||
@@ -475,6 +477,38 @@ void node_layers::forget(basic_node const *Node)
|
||||
m_sources.erase(lookup);
|
||||
}
|
||||
|
||||
void node_layers::shape(material_handle const Material, source_span const &Span)
|
||||
{
|
||||
auto const current{handle()};
|
||||
if (false == valid(current) || false == Span.valid() || m_instance != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shapes.push_back({current, Span, Material, false});
|
||||
}
|
||||
|
||||
std::pair<std::size_t, std::size_t> node_layers::erase_shapes(std::set<material_handle> const &Materials)
|
||||
{
|
||||
std::pair<std::size_t, std::size_t> result{0, 0};
|
||||
for (auto &shape : m_shapes)
|
||||
{
|
||||
if (shape.erased || Materials.count(shape.material) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (false == writable(shape.layer))
|
||||
{
|
||||
++result.second;
|
||||
continue;
|
||||
}
|
||||
shape.erased = true;
|
||||
m_erased.emplace_back(shape.layer, shape.span);
|
||||
m_shapeserased = true;
|
||||
++result.first;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool node_layers::is_descendant(layer_handle Layer, layer_handle const Ancestor) const
|
||||
{
|
||||
// NOTE: content of a merged layer, includes of other files among it, belongs to the layer it was merged into
|
||||
|
||||
@@ -135,10 +135,20 @@ struct node_source
|
||||
glm::vec3 scale{1.f};
|
||||
};
|
||||
|
||||
// definition of a shape (triangles) in a scenery layer file
|
||||
struct shape_source
|
||||
{
|
||||
layer_handle layer{null_handle};
|
||||
source_span span;
|
||||
material_handle material{null_handle};
|
||||
bool erased{false}; // the definition goes from the file on save
|
||||
};
|
||||
|
||||
struct save_result
|
||||
{
|
||||
bool success{false};
|
||||
std::vector<std::string> files; // names of saved files
|
||||
std::vector<std::string> backups; // copies of the files made before they lost their ground
|
||||
std::string message; // summary, or cause of the failure
|
||||
};
|
||||
|
||||
@@ -249,6 +259,15 @@ class node_layers
|
||||
void track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles = glm::vec3{0.f}, glm::vec3 const &Scale = glm::vec3{1.f});
|
||||
// indicates specified node was removed from the scene
|
||||
void forget(basic_node const *Node);
|
||||
// stores location of a shape defined directly in the layer file being loaded
|
||||
void shape(material_handle const Material, source_span const &Span);
|
||||
std::vector<shape_source> const &shapes() const
|
||||
{
|
||||
return m_shapes;
|
||||
}
|
||||
// drops the definitions of the shapes of specified materials from the writable layer files on save.
|
||||
// returns: amount of the shapes to drop, and of the ones which stay as their files can't be rewritten
|
||||
std::pair<std::size_t, std::size_t> erase_shapes(std::set<material_handle> const &Materials);
|
||||
// indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive.
|
||||
// Infile: the directive comes straight from the layer file being loaded
|
||||
void initialization(source_span const &Span, bool Infile);
|
||||
@@ -354,6 +373,8 @@ class node_layers
|
||||
layer_context m_context; // placement in effect at the current point of scenery load
|
||||
std::unordered_map<basic_node const *, node_source> m_sources; // origins of nodes which can be rewritten on save
|
||||
std::vector<std::pair<layer_handle, source_span>> m_erased; // definitions of nodes deleted since the load or the last save
|
||||
std::vector<shape_source> m_shapes;
|
||||
bool m_shapeserased{false}; // the binary terrain file holds shapes which the save drops
|
||||
std::set<std::string> m_templates; // *.inc templates used by the scenery
|
||||
std::vector<include_instance> m_instances; // includes of the templates; instance handle is index in the vector + 1
|
||||
instance_handle m_instance{0}; // helper, include whose template is being loaded
|
||||
|
||||
@@ -1451,6 +1451,34 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||
}
|
||||
written.emplace_back(output);
|
||||
}
|
||||
// a file which loses its ground to the terrain of the editor keeps a copy of itself each time, whatever was saved before.
|
||||
// without the copy nothing is replaced
|
||||
{
|
||||
auto const now{std::time(nullptr)};
|
||||
char stamp[32];
|
||||
std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", std::localtime(&now));
|
||||
for (auto const output : written)
|
||||
{
|
||||
auto const filepath{path(output)};
|
||||
bool const losesground{std::any_of(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return Shape.erased && Shape.layer == output; })};
|
||||
if (false == losesground || false == std::filesystem::exists(filepath, error))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto const backup{filepath + ".bak-" + stamp + "-przed-terenem-edytora"};
|
||||
std::error_code copyerror;
|
||||
if (false == std::filesystem::copy_file(filepath, backup, std::filesystem::copy_options::overwrite_existing, copyerror))
|
||||
{
|
||||
for (auto const leftover : written)
|
||||
{
|
||||
std::filesystem::remove(path(leftover) + ".tmp", error);
|
||||
}
|
||||
return fail("can't make a backup of file \"" + layer(output).name + "\": " + copyerror.message());
|
||||
}
|
||||
WriteLog("Scenery save: the triangles of the ground go from \"" + layer(output).name + "\", its previous content is kept in \"" + backup + "\"");
|
||||
result.backups.emplace_back(backup);
|
||||
}
|
||||
}
|
||||
std::vector<layer_handle> replaced;
|
||||
for (auto const output : written)
|
||||
{
|
||||
@@ -1586,6 +1614,8 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||
included.dead = included.dead || included.removed;
|
||||
}
|
||||
m_erased.clear();
|
||||
// the shapes of the rewritten files are somewhere else in them now
|
||||
m_shapes.erase(std::remove_if(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return std::find(std::begin(composed), std::end(composed), Shape.layer) != std::end(composed); }), std::end(m_shapes));
|
||||
for (auto *path : savedpaths)
|
||||
{
|
||||
path->m_dirty = false;
|
||||
@@ -1622,6 +1652,8 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||
{
|
||||
m_sources.erase(point);
|
||||
}
|
||||
removedterrain |= m_shapeserased;
|
||||
m_shapeserased = false;
|
||||
if (removedterrain)
|
||||
{
|
||||
// binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load
|
||||
@@ -1649,6 +1681,10 @@ save_result node_layers::save(std::vector<std::string> const &Rootstatements)
|
||||
{
|
||||
result.message += file + (&file != &result.files.back() ? ", " : "");
|
||||
}
|
||||
for (auto const &backup : result.backups)
|
||||
{
|
||||
result.message += ". Backup before the ground was taken out: " + std::filesystem::path(backup).filename().string();
|
||||
}
|
||||
}
|
||||
if (false == unwritten.empty())
|
||||
{
|
||||
|
||||
@@ -678,17 +678,32 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
|
||||
&& Input.Name().starts_with("scenery/zwr")
|
||||
&& Input.Name().ends_with(".inc") ) };
|
||||
|
||||
material_handle material { null_handle };
|
||||
if( false == skip ) {
|
||||
|
||||
auto shape { scene::shape_node().import( Input, nodedata ) };
|
||||
material = shape.data().material;
|
||||
simulation::Region->insert(
|
||||
scene::shape_node().import(
|
||||
Input, nodedata ),
|
||||
std::move( shape ),
|
||||
Scratchpad,
|
||||
true );
|
||||
}
|
||||
else {
|
||||
if( nodedata.layer != null_handle && Input.InLayerFile() ) {
|
||||
// the editor wants to know the material even of the shapes which come from the binary terrain
|
||||
auto token { Input.getToken<std::string>() };
|
||||
if( token == "material" ) {
|
||||
skip_until( Input, "endmaterial" );
|
||||
token = Input.getToken<std::string>();
|
||||
}
|
||||
replace_slashes( token );
|
||||
material = GfxRenderer->Fetch_Material( token );
|
||||
}
|
||||
skip_until( Input, "endtri" );
|
||||
}
|
||||
if( nodedata.layer != null_handle && Input.InLayerFile() ) {
|
||||
scene::Layers.shape( material, { sourcebegin, Input.TokenEnd() } );
|
||||
}
|
||||
}
|
||||
else if( nodedata.type == "lines"
|
||||
|| nodedata.type == "line_strip"
|
||||
@@ -968,7 +983,7 @@ void
|
||||
state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data &Scratchpad)
|
||||
{
|
||||
// editor-authored streaming terrain. format:
|
||||
// editorterrain <folder> <cells> <cellsize> <radius> endeditorterrain
|
||||
// editorterrain <folder> <cells> <cellsize> <radius> [texture] endeditorterrain
|
||||
// the global streamer loads its 16-bit chunk files around the camera in every mode
|
||||
std::string folder;
|
||||
int cells = 32;
|
||||
@@ -976,13 +991,19 @@ state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data
|
||||
int radius = 4;
|
||||
Input.getTokens(4);
|
||||
Input >> folder >> cells >> cellsize >> radius;
|
||||
skip_until(Input, "endeditorterrain");
|
||||
std::string texture { Input.getToken<std::string>() };
|
||||
if( texture == "endeditorterrain" ) {
|
||||
texture.clear();
|
||||
}
|
||||
else {
|
||||
skip_until(Input, "endeditorterrain");
|
||||
}
|
||||
scene::Layers.terrain_directive(true);
|
||||
|
||||
if (!folder.empty() && cells > 0 && cellsize > 0.0f)
|
||||
{
|
||||
EditorTerrain.directory(folder);
|
||||
EditorTerrain.configure(cells, cellsize, radius < 0 ? 0 : radius, 0.0f, std::string());
|
||||
EditorTerrain.configure(cells, cellsize, radius < 0 ? 0 : radius, 0.0f, texture);
|
||||
EditorTerrain.active(true);
|
||||
WriteLog("Editor terrain stream enabled: " + folder + " (cells " + std::to_string(cells)
|
||||
+ ", cellsize " + std::to_string(cellsize) + ", radius " + std::to_string(radius) + ")",
|
||||
@@ -1418,7 +1439,9 @@ state_serializer::export_as_text(std::string const &Scenariofile) const {
|
||||
<< EditorTerrain.directory() << ' '
|
||||
<< EditorTerrain.cells() << ' '
|
||||
<< EditorTerrain.cellsize() << ' '
|
||||
<< EditorTerrain.radius() << " endeditorterrain\n";
|
||||
<< EditorTerrain.radius()
|
||||
<< ( EditorTerrain.texture().empty() ? "" : " " + EditorTerrain.texture() )
|
||||
<< " endeditorterrain\n";
|
||||
}
|
||||
|
||||
scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n";
|
||||
|
||||
Reference in New Issue
Block a user