17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-30 23:40:29 +01:00

Editor terrain: texture browser, undo, conversion from terrain files

- Terrain work mode shows a browser of the textures folder in place of the
  node bank; textures/materials can be dragged onto the palette entries
  (replacing the material), the material field and the water material field
- Ctrl+Z/Ctrl+Y for heightmap terrain changes: a brush stroke or an
  operation is one step of the editor history
- Ground conversion reads only the files of the terrain directives
  (.txtf with origin/rotate, or .btf without text), paints the chunks with
  the materials of the triangles, drops only the shapes of those files
- Fix: ground grids converted out-of-range floats to int, so whole-scene
  queries returned the triangles of a single cell (shifted chunks)
- Fix: terrain layers copied from textures already uploaded without their
  data stayed grey
- Fix: ImGui ID conflict between terrain tool radios and section headers
- Fix: layers of terrain .txtf files named with the scenery/ prefix and
  could not be saved
- Editor selftest commands for the terrain
This commit is contained in:
2026-10-09 23:39:26 +02:00
parent 5716bc1669
commit 8cd0bab27b
21 changed files with 1211 additions and 145 deletions

View File

@@ -191,6 +191,7 @@ set(SOURCES
"application/editormodetrackux.cpp" "application/editormodetrackux.cpp"
"application/editormoderoad.cpp" "application/editormoderoad.cpp"
"application/editormodeprofile.cpp" "application/editormodeprofile.cpp"
"application/editormodetextures.cpp"
"application/editormodegauge.cpp" "application/editormodegauge.cpp"
"application/editormodeinfra.cpp" "application/editormodeinfra.cpp"
"application/editormodespeed.cpp" "application/editormodespeed.cpp"

View File

@@ -45,6 +45,7 @@ extern char **environ;
#include "utilities/Float3d.h" #include "utilities/Float3d.h"
#include "scene/scene.h" #include "scene/scene.h"
#include "scene/scenelayers.h" #include "scene/scenelayers.h"
#include "scene/sceneterrain.h"
#include "utilities/parser.h" #include "utilities/parser.h"
#include "utilities/utilities.h" #include "utilities/utilities.h"
@@ -213,8 +214,8 @@ namespace
} }
} }
// highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one // highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one, and which triangle it is in Index if asked
bool height_at(double const X, double const Z, double const Ceiling, double &OutY) const bool height_at(double const X, double const Z, double const Ceiling, double &OutY, std::uint32_t *Index = nullptr) const
{ {
bool found = false; bool found = false;
for (auto const idx : m_cells[static_cast<std::size_t>(row(Z)) * m_columns + column(X)]) for (auto const idx : m_cells[static_cast<std::size_t>(row(Z)) * m_columns + column(X)])
@@ -225,6 +226,8 @@ namespace
{ {
OutY = y; OutY = y;
found = true; found = true;
if (Index != nullptr)
*Index = idx;
} }
} }
return found; return found;
@@ -233,11 +236,11 @@ namespace
private: private:
int column(double const X) const int column(double const X) const
{ {
return std::clamp(static_cast<int>(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1); return static_cast<int>(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast<double>(m_columns - 1)));
} }
int row(double const Z) const int row(double const Z) const
{ {
return std::clamp(static_cast<int>(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1); return static_cast<int>(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast<double>(m_rows - 1)));
} }
std::vector<world_triangle> m_triangles; std::vector<world_triangle> m_triangles;
@@ -498,6 +501,7 @@ editor_mode::editor_mode() {
ui()->set_track_options([this]() { render_track_tool_options(); }, [this]() { render_track_selection(); }); ui()->set_track_options([this]() { render_track_tool_options(); }, [this]() { render_track_selection(); });
ui()->set_workspace_options(editor_ui::workspace::roads, [this]() { render_road_tool_options(); }, [this]() { render_road_selection(); }); ui()->set_workspace_options(editor_ui::workspace::roads, [this]() { render_road_tool_options(); }, [this]() { render_road_selection(); });
ui()->set_workspace_options(editor_ui::workspace::terrain, [this]() { render_terrain_tool_options(); }); ui()->set_workspace_options(editor_ui::workspace::terrain, [this]() { render_terrain_tool_options(); });
ui()->set_workspace_bank(editor_ui::workspace::terrain, [this]() { render_texture_browser(); }, STR("Textures"));
// a node picked in the scene window is selected as if clicked in the view; a double click flies the camera to it // a node picked in the scene window is selected as if clicked in the view; a double click flies the camera to it
ui()->set_scene_select([this](scene::basic_node *Node, bool const Focus) { ui()->set_scene_select([this](scene::basic_node *Node, bool const Focus) {
if (Focus) if (Focus)
@@ -876,8 +880,50 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node)
m_array.base = nullptr; m_array.base = nullptr;
} }
void editor_mode::terrain_history_step()
{
if (false == m_streamer.active())
return;
if (m_streamer.recording())
{
if (terrain_brush() && mouseHold && false == m_history_busy)
return; // the stroke goes on
auto change{m_streamer.end_change()};
if (false == change.empty())
{
trim_history();
EditorSnapshot snap;
snap.action = EditorSnapshot::Action::Terrain;
snap.node_name = STR("terrain");
if (false == change.chunks.empty())
{
auto const centre{heightmap::chunk_corner(change.chunks.begin()->first) + glm::dvec2{heightmap::chunk_size * 0.5}};
snap.position = {centre.x, 0.0, centre.y};
}
snap.terrain = std::make_shared<terrain_streamer::change const>(std::move(change));
m_history.push_back(std::move(snap));
g_redo.clear();
}
}
m_streamer.begin_change();
}
void editor_mode::restore_terrain(EditorSnapshot &Snapshot)
{
if (Snapshot.terrain == nullptr)
return;
auto opposite{m_streamer.restore(*Snapshot.terrain)};
Snapshot.terrain = std::make_shared<terrain_streamer::change const>(std::move(opposite));
forget_ground();
m_water_selected = -1;
}
void editor_mode::undo_last() void editor_mode::undo_last()
{ {
// a change of the terrain made a moment ago is a step of its own before it's taken back
m_history_busy = true;
terrain_history_step();
m_history_busy = false;
// the changes made together are taken back together, and their models are the selection again // the changes made together are taken back together, and their models are the selection again
std::vector<scene::basic_node *> touched; std::vector<scene::basic_node *> touched;
while (false == m_history.empty()) while (false == m_history.empty())
@@ -906,6 +952,13 @@ void editor_mode::undo_one()
EditorSnapshot snap = m_history.back(); EditorSnapshot snap = m_history.back();
m_history.pop_back(); m_history.pop_back();
if (snap.action == EditorSnapshot::Action::Terrain)
{
restore_terrain(snap);
g_redo.push_back(std::move(snap));
return;
}
if (snap.action == EditorSnapshot::Action::Bend) if (snap.action == EditorSnapshot::Action::Bend)
{ {
restore_bend(snap, true); restore_bend(snap, true);
@@ -1036,6 +1089,10 @@ void editor_mode::undo_one()
void editor_mode::redo_last() void editor_mode::redo_last()
{ {
// a change of the terrain made since the undo goes to the history, and the redo steps are dropped with it
m_history_busy = true;
terrain_history_step();
m_history_busy = false;
// the first of the changes made together is on the top, the others follow it // the first of the changes made together is on the top, the others follow it
std::vector<scene::basic_node *> touched; std::vector<scene::basic_node *> touched;
do do
@@ -1060,6 +1117,13 @@ void editor_mode::redo_one()
EditorSnapshot snap = g_redo.back(); EditorSnapshot snap = g_redo.back();
g_redo.pop_back(); g_redo.pop_back();
if (snap.action == EditorSnapshot::Action::Terrain)
{
restore_terrain(snap);
m_history.push_back(std::move(snap));
return;
}
if (snap.action == EditorSnapshot::Action::Bend) if (snap.action == EditorSnapshot::Action::Bend)
{ {
restore_bend(snap, false); restore_bend(snap, false);
@@ -1238,6 +1302,7 @@ bool editor_mode::update()
m_dragging = false; m_dragging = false;
m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE); m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE);
} }
terrain_history_step();
// fixed step render time routines (50 Hz) // fixed step render time routines (50 Hz)
fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable
@@ -1479,8 +1544,8 @@ namespace
return changed; return changed;
} }
// grid points without a height take the height of the nearest point which has one (by steps along the grid) // grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid)
void fill_from_nearest(std::vector<double> &Heights, std::vector<char> &Found, std::size_t const Side) template <typename Value_> void fill_from_nearest(std::vector<Value_> &Heights, std::vector<char> &Found, std::size_t const Side)
{ {
std::deque<std::size_t> queue; std::deque<std::size_t> queue;
for (std::size_t index = 0; index < Found.size(); ++index) for (std::size_t index = 0; index < Found.size(); ++index)
@@ -1530,6 +1595,8 @@ void editor_mode::render_terrain_ui()
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", Tooltip); ImGui::SetTooltip("%s", Tooltip);
}; };
// the tools are named like the headers below, which would give them the same IDs
ImGui::PushID("terrain_tools");
radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings")); radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings"));
ImGui::SameLine(); ImGui::SameLine();
radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk")); radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk"));
@@ -1542,6 +1609,7 @@ void editor_mode::render_terrain_ui()
radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette")); radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"));
ImGui::SameLine(); ImGui::SameLine();
radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back")); radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"));
ImGui::PopID();
m_terrain_tool = static_cast<terrain_tool>(tool); m_terrain_tool = static_cast<terrain_tool>(tool);
if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen)) if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen))
@@ -1594,6 +1662,10 @@ void editor_mode::render_terrain_ui()
auto const label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())}; auto const label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())};
if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == m_terrain_layer)) if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == m_terrain_layer))
m_terrain_layer = static_cast<int>(i); m_terrain_layer = static_cast<int>(i);
// a material dropped on an entry takes its place, on every chunk painted with it
std::string dropped;
if (material_drop(dropped))
m_streamer.layer_material(static_cast<std::uint16_t>(i), dropped);
} }
if (false == layers.empty()) if (false == layers.empty())
{ {
@@ -1607,10 +1679,12 @@ void editor_mode::render_terrain_ui()
} }
ImGui::SetNextItemWidth(160.0f); ImGui::SetNextItemWidth(160.0f);
ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material)); ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material));
material_drop(m_terrain_material, IM_ARRAYSIZE(m_terrain_material));
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0') if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0')
m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size); m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size);
ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them.")); ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them. "
"A texture dragged from the browser onto an entry of the palette takes its place."));
} }
if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0)) if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0))
@@ -1623,6 +1697,7 @@ void editor_mode::render_terrain_ui()
m_water_level_edited = true; m_water_level_edited = true;
ImGui::SetNextItemWidth(160.0f); ImGui::SetNextItemWidth(160.0f);
ImGui::InputText(STR_C("Material##water"), m_water_material, IM_ARRAYSIZE(m_water_material)); ImGui::InputText(STR_C("Material##water"), m_water_material, IM_ARRAYSIZE(m_water_material));
material_drop(m_water_material, IM_ARRAYSIZE(m_water_material));
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
ImGui::SetTooltip(STR_C("Empty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str()); ImGui::SetTooltip(STR_C("Empty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str());
ImGui::BeginDisabled(m_water_points.size() < 3); ImGui::BeginDisabled(m_water_points.size() < 3);
@@ -2254,41 +2329,13 @@ void editor_mode::handle_terrain_click(bool const Shift)
namespace namespace
{ {
std::uint64_t geometry_key(gfx::geometry_handle const &Geometry) // takes the shapes of specified terrain files and materials out of the scene. returns: the count of the shapes
{ std::size_t drop_terrain_shapes(std::set<std::uint16_t> const &Files, std::set<material_handle> const &Materials)
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; std::size_t dropped = 0;
auto const dropping = [&](scene::shape_node const &Shape) { auto const dropping = [&](scene::shape_node const &Shape) {
auto const &data = Shape.data(); 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) && bool const drop = Files.count(data.terrainfile) != 0 && Materials.count(data.material) != 0;
false == owned_shapes::owns(data.geometry);
dropped += drop ? 1 : 0; dropped += drop ? 1 : 0;
return drop; return drop;
}; };
@@ -2299,33 +2346,63 @@ namespace
continue; continue;
section->m_shapes.erase(std::remove_if(section->m_shapes.begin(), section->m_shapes.end(), dropping), section->m_shapes.end()); 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) 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()); cell.m_shapesopaque.erase(std::remove_if(cell.m_shapesopaque.begin(), cell.m_shapesopaque.end(), dropping), cell.m_shapesopaque.end());
cell.m_shapestranslucent.erase(std::remove_if(cell.m_shapestranslucent.begin(), cell.m_shapestranslucent.end(), dropping), cell.m_shapestranslucent.end());
}
} }
return dropped; return dropped;
} }
} // namespace } // namespace
std::map<material_handle, std::size_t> editor_mode::ground_materials() void editor_mode::load_ground_files()
{ {
std::map<material_handle, std::size_t> result; auto &conversion{m_ground_conversion};
if (simulation::Region == nullptr) conversion.files.clear();
return result; conversion.triangles.clear();
std::set<std::uint64_t> own; conversion.materials.clear();
for (auto *terrain : active_terrains()) conversion.names.clear();
own.insert(geometry_key(terrain->geometry())); conversion.chosen.clear();
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5}; std::map<std::string, std::uint16_t> lookup;
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) { auto const &references{scene::terrain_file::references()};
std::vector<world_triangle> triangles; for (std::size_t index = 0; index < references.size(); ++index)
gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true); {
if (false == triangles.empty()) ground_conversion::source source;
result[Shape.data().material] += triangles.size(); source.file = references[index].file;
}); source.reference = static_cast<std::uint16_t>(index + 1);
return result; source.first = conversion.triangles.size();
source.good = scene::terrain_file::read_triangles(
references[index],
[&](std::string const &Material, std::array<glm::dvec3, 3> const &Triangle) {
auto found{lookup.find(Material)};
if (found == lookup.end())
{
found = lookup.emplace(Material, static_cast<std::uint16_t>(conversion.names.size())).first;
conversion.names.push_back(Material);
}
conversion.triangles.push_back(Triangle);
conversion.materials.push_back(found->second);
},
&source.message, &source.text);
if (false == source.good)
{
// what was read before the failure doesn't count
conversion.triangles.resize(source.first);
conversion.materials.resize(source.first);
source.chosen = false;
}
source.count = conversion.triangles.size() - source.first;
conversion.files.push_back(std::move(source));
}
// the terrain files hold the ground, all of it is taken unless told otherwise
for (std::size_t index = 0; index < conversion.names.size(); ++index)
conversion.chosen.insert(index);
} }
std::string editor_mode::convert_ground(std::set<material_handle> const &Materials) std::string editor_mode::convert_ground()
{ {
if (simulation::Region == nullptr || Materials.empty()) auto &conversion{m_ground_conversion};
if (simulation::Region == nullptr || conversion.chosen.empty())
return {}; return {};
if (false == ensure_terrain()) if (false == ensure_terrain())
return STR_C("The heightmap terrain can't be opened"); return STR_C("The heightmap terrain can't be opened");
@@ -2333,50 +2410,56 @@ std::string editor_mode::convert_ground(std::set<material_handle> const &Materia
int const cells = heightmap::cells_for(m_terrain_spacing); int const cells = heightmap::cells_for(m_terrain_spacing);
double const cellsize = size / cells; double const cellsize = size / cells;
// the shapes of the heightmap terrain hold no triangles of their own, nothing to leave out // the chosen triangles of the chosen files
std::set<std::uint64_t> const own;
std::vector<world_triangle> triangles; std::vector<world_triangle> triangles;
std::vector<material_handle> materials; std::vector<std::uint16_t> materials;
glm::dvec2 const everywhere{std::numeric_limits<double>::max() * 0.5}; std::set<std::uint16_t> files;
visit_scenery_shapes(own, [&](scene::shape_node const &Shape) { for (auto const &source : conversion.files)
if (Materials.count(Shape.data().material) == 0) {
return; if (false == source.good || false == source.chosen)
gather_shape_triangles(Shape, -everywhere, everywhere, triangles, true); continue;
materials.resize(triangles.size(), Shape.data().material); files.insert(source.reference);
}); for (auto index = source.first; index < source.first + source.count; ++index)
{
if (conversion.chosen.count(conversion.materials[index]) == 0)
continue;
triangles.push_back(conversion.triangles[index]);
materials.push_back(conversion.materials[index]);
}
}
if (triangles.empty()) if (triangles.empty())
return STR_C("The chosen materials have no triangles"); return STR_C("The chosen files and materials have no triangles");
// the triangles reaching each chunk // the triangles reaching each chunk
std::map<std::pair<int, int>, std::vector<std::uint32_t>> buckets; std::map<heightmap::chunk_key, std::vector<std::uint32_t>> buckets;
for (std::uint32_t i = 0; i < triangles.size(); ++i) for (std::uint32_t i = 0; i < triangles.size(); ++i)
{ {
auto const &t = triangles[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)); // a triangle which only touches the edge of a chunk gives it nothing but the edge, which the chunk on the other side has
int const x1 = static_cast<int>(std::floor(std::max({t[0].x, t[1].x, t[2].x}) / size)); auto const margin{1e-3};
int const z0 = static_cast<int>(std::floor(std::min({t[0].z, t[1].z, t[2].z}) / size)); auto const low{heightmap::chunk_at(std::min({t[0].x, t[1].x, t[2].x}) + margin, std::min({t[0].z, t[1].z, t[2].z}) + margin)};
int const z1 = static_cast<int>(std::floor(std::max({t[0].z, t[1].z, t[2].z}) / size)); auto const high{heightmap::chunk_at(std::max({t[0].x, t[1].x, t[2].x}) - margin, std::max({t[0].z, t[1].z, t[2].z}) - margin)};
for (int x = x0; x <= x1; ++x) for (int x = low.first; x <= high.first; ++x)
for (int z = z0; z <= z1; ++z) for (int z = low.second; z <= high.second; ++z)
buckets[{x, z}].push_back(i); buckets[{x, z}].push_back(i);
} }
// the chunks are covered with the material which covers the most; it goes to the palette of the terrain // palette entries of the materials, added as they're needed
std::map<material_handle, double> coverage; std::map<std::uint16_t, std::uint16_t> palette;
for (std::size_t i = 0; i < triangles.size(); ++i) auto const layer_of = [&](std::uint16_t const Material) {
{ auto const found{palette.find(Material)};
auto const &t = triangles[i]; if (found != palette.end())
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)); return found->second;
} auto const layer{m_streamer.layer(conversion.names[Material])};
auto const dominant = std::max_element(coverage.begin(), coverage.end(), [](auto const &A, auto const &B) { return A.second < B.second; })->first; palette.emplace(Material, layer);
std::uint16_t layer = 0; return layer;
if (auto const *material = GfxRenderer->Material(dominant)) };
layer = m_streamer.layer(material->GetName());
// 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 made = 0, skipped = 0;
std::size_t const side = static_cast<std::size_t>(cells) + 1; std::size_t const side = static_cast<std::size_t>(cells) + 1;
auto const samples{heightmap::default_paint_samples};
std::size_t const paintside = static_cast<std::size_t>(samples) + 1;
double const paintstep = size / samples;
for (auto const &bucket : buckets) for (auto const &bucket : buckets)
{ {
auto const &key = bucket.first; auto const &key = bucket.first;
@@ -2385,11 +2468,17 @@ std::string editor_mode::convert_ground(std::set<material_handle> const &Materia
++skipped; ++skipped;
continue; continue;
} }
glm::dvec2 const low{key.first * size, key.second * size}; auto const low{heightmap::chunk_corner(key)};
std::vector<world_triangle> local; std::vector<world_triangle> local;
std::vector<std::uint16_t> localmaterials;
for (auto const i : bucket.second) for (auto const i : bucket.second)
{
local.push_back(triangles[i]); local.push_back(triangles[i]);
localmaterials.push_back(materials[i]);
}
triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0}); triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0});
auto const ceiling{std::numeric_limits<double>::max()};
// heights of the grid points; where there's no ground they're taken from the nearest point which has some
std::vector<double> heights(side * side, 0.0); std::vector<double> heights(side * side, 0.0);
std::vector<char> found(side * side, 0); std::vector<char> found(side * side, 0);
bool any = false; bool any = false;
@@ -2397,29 +2486,103 @@ std::string editor_mode::convert_ground(std::set<material_handle> const &Materia
for (std::size_t ix = 0; ix < side; ++ix) for (std::size_t ix = 0; ix < side; ++ix)
{ {
auto const index = iz * 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])) if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, ceiling, heights[index]))
found[index] = 1, any = true; found[index] = 1, any = true;
} }
if (false == any) if (false == any)
continue; continue;
fill_from_nearest(heights, found, side); fill_from_nearest(heights, found, side);
auto const sampler = [&](double X, double Z) { // the material on top at each paint sample
auto const ix = static_cast<std::size_t>(std::clamp<long>(std::lround((X - low.x) / cellsize), 0, cells)); std::vector<std::uint16_t> paint(paintside * paintside, 0);
auto const iz = static_cast<std::size_t>(std::clamp<long>(std::lround((Z - low.y) / cellsize), 0, cells)); std::vector<char> painted(paintside * paintside, 0);
return static_cast<float>(heights[iz * side + ix]); std::map<std::uint16_t, std::size_t> counts;
}; for (std::size_t iz = 0; iz < paintside; ++iz)
if (m_streamer.add_chunk(key.first, key.second, m_terrain_spacing, sampler, layer)) for (std::size_t ix = 0; ix < paintside; ++ix)
{
double height;
std::uint32_t triangle;
if (false == ground.height_at(low.x + ix * paintstep, low.y + iz * paintstep, ceiling, height, &triangle))
continue;
auto const index = iz * paintside + ix;
paint[index] = localmaterials[triangle];
painted[index] = 1;
++counts[paint[index]];
}
if (counts.empty())
{
// the points of the grid hit the ground, the samples in between didn't; the nearest triangle covers the chunk
counts[localmaterials.front()] = 1;
std::fill(paint.begin(), paint.end(), localmaterials.front());
}
else
fill_from_nearest(paint, painted, paintside);
// a chunk holds up to 8 materials, the ones covering the most; the rest is given to the one covering the most
std::vector<std::pair<std::size_t, std::uint16_t>> ranking;
for (auto const &count : counts)
ranking.emplace_back(count.second, count.first);
std::sort(ranking.begin(), ranking.end(), [](auto const &A, auto const &B) { return A.first > B.first; });
if (ranking.size() > heightmap::max_chunk_layers)
ranking.resize(heightmap::max_chunk_layers);
auto data{heightmap::chunk_data::make_flat(key, m_terrain_spacing, 0.f)};
for (std::size_t index = 0; index < data->heights.size(); ++index)
data->heights[index] = static_cast<float>(heights[index]);
std::map<std::uint16_t, std::size_t> slots;
for (std::size_t slot = 0; slot < ranking.size(); ++slot)
{
data->layers[slot] = layer_of(ranking[slot].second);
slots.emplace(ranking[slot].second, slot);
}
if (ranking.size() > 1)
{
auto const plane{paintside * paintside};
data->paint = samples;
data->weights.assign(plane * ranking.size(), 0);
for (std::size_t index = 0; index < plane; ++index)
{
auto const slot{slots.find(paint[index])};
data->weights[(slot != slots.end() ? slot->second : 0) * plane + index] = 255;
}
}
if (m_streamer.add_chunk(std::move(data)))
++made; ++made;
} }
ground_tiles.clear(); forget_ground();
forget_ground_shapes();
auto const erased = scene::Layers.erase_shapes(Materials); // the converted ground goes from the scene, and from the text files on save
auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the scenery files on save"), made, dropped, erased.first); std::set<material_handle> handles;
for (auto const material : conversion.chosen)
if (material < conversion.names.size())
handles.insert(GfxRenderer->Fetch_Material(conversion.names[material]));
auto const dropped = drop_terrain_shapes(files, handles);
std::set<scene::layer_handle> layers;
std::size_t binary = 0;
for (auto const &source : conversion.files)
{
if (false == source.good || false == source.chosen)
continue;
if (false == source.text)
{
++binary;
continue;
}
auto const textfile{source.file + ".txtf"};
for (std::size_t layer = 1; layer <= scene::Layers.size(); ++layer)
{
auto name{scene::Layers.layer(static_cast<scene::layer_handle>(layer)).name};
replace_slashes(name);
if (name.ends_with(textfile))
layers.insert(static_cast<scene::layer_handle>(layer));
}
}
auto const erased = scene::Layers.erase_shapes(handles, &layers);
auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the terrain files on save"), made, dropped, erased.first);
if (skipped > 0) if (skipped > 0)
summary += format(STR_C(", %zu chunks kept as there was terrain already"), skipped); summary += format(STR_C(", %zu chunks kept as there was terrain already"), skipped);
if (erased.second > 0) if (erased.second > 0)
summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second); summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second);
if (binary > 0)
summary += format(STR_C(". %zu of the files are binary (.btf) without their text: their triangles come back with the next load, until the files are taken out of the terrain directive"), binary);
WriteLog("Editor: " + summary, logtype::generic); WriteLog("Editor: " + summary, logtype::generic);
return summary; return summary;
} }
@@ -5261,7 +5424,8 @@ void editor_mode::render_change_history(){
s.action == EditorSnapshot::Action::Scale ? "SCA" : s.action == EditorSnapshot::Action::Scale ? "SCA" :
s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : s.action == EditorSnapshot::Action::TrackEdit ? "TRK" :
s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" :
s.action == EditorSnapshot::Action::Array ? "ARR" : "OTH", s.action == EditorSnapshot::Action::Array ? "ARR" :
s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH",
s.node_name.empty() ? "(noname)" : s.node_name.c_str(), s.node_name.empty() ? "(noname)" : s.node_name.c_str(),
s.position.x, s.position.y, s.position.z); s.position.x, s.position.y, s.position.z);

View File

@@ -132,7 +132,7 @@ class editor_mode : public application_mode, private editor_track::observer
struct stored_profile; struct stored_profile;
struct EditorSnapshot struct EditorSnapshot
{ {
enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend }; enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain };
// copy of a model an array made // copy of a model an array made
struct array_copy struct array_copy
{ {
@@ -170,6 +170,7 @@ class editor_mode : public application_mode, private editor_track::observer
std::vector<stored_profile> profile_library; std::vector<stored_profile> profile_library;
std::vector<array_copy> copies; // models an array added to the one it was made of std::vector<array_copy> copies; // models an array added to the one it was made of
bool joined{false}; // undone and redone together with the change before it bool joined{false}; // undone and redone together with the change before it
std::shared_ptr<terrain_streamer::change const> terrain; // the heightmap terrain from before the change (Action::Terrain)
}; };
void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string());
@@ -183,6 +184,12 @@ class editor_mode : public application_mode, private editor_track::observer
// a single step of the history, see undo_last() // a single step of the history, see undo_last()
void undo_one(); void undo_one();
void redo_one(); void redo_one();
// the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush,
// or what a frame did; the next change is recorded from now on
void terrain_history_step();
// puts the terrain back as the step recorded it, the step left with the state it replaced
void restore_terrain(EditorSnapshot &Snapshot);
bool m_history_busy{false}; // undo or redo under way
void handle_brush_mouse_hold(int Action, int Button); void handle_brush_mouse_hold(int Action, int Button);
void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value);
// members // members
@@ -313,18 +320,59 @@ class editor_mode : public application_mode, private editor_track::observer
void draw_terrain_overlay(); void draw_terrain_overlay();
// forgets what's known about the ground, after the terrain changed // forgets what's known about the ground, after the terrain changed
void forget_ground(); void forget_ground();
// the scenery ground: triangles of each material, the ones the editor makes itself left out // conversion of the ground of the terrain files of the scenery (terrain directive, .txtf/.btf) to heightmap terrain
std::map<material_handle, std::size_t> ground_materials();
// replaces the triangles of specified materials with chunks of heightmap 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 struct ground_conversion
{ {
struct source
{
std::string file; // as the terrain directive names it
std::uint16_t reference{0}; // see scene::terrain_file::reference_of()
bool text{false}; // read from the text file, rather than the binary one
bool good{false};
bool chosen{true};
std::string message;
std::size_t first{0}, count{0}; // its triangles
};
bool open{false}; bool open{false};
std::map<material_handle, std::size_t> materials; std::vector<source> files;
std::set<material_handle> chosen; std::vector<std::array<glm::dvec3, 3>> triangles;
std::vector<std::uint16_t> materials; // of the triangles: index of the name
std::vector<std::string> names; // of the materials
std::set<std::size_t> chosen; // materials to convert
std::string status; std::string status;
} m_ground_conversion; } m_ground_conversion;
// reads the triangles of the terrain files of the scenery for the conversion
void load_ground_files();
// the folder of the textures as a browser, in the window of the node bank while the terrain is edited: the textures and the
// materials can be dragged onto the fields which take a material
struct texture_browser
{
struct item
{
std::string name; // as the materials are named: relative to the folder of the textures, without the extension
std::string label; // the file, without the folder
bool material{false}; // there's a .mat file of the name
};
std::string folder; // relative to the folder of the textures, empty or with a trailing slash
std::vector<std::string> folders; // under the current one
std::vector<item> items;
bool listed{false};
char filter[64]{};
float size{96.f}; // of the images, pixels
std::string selected;
std::map<std::string, int> thumbnails; // texture shown for the item of the name, 0: none
int loads{0}; // textures read in the frame, a few at most so the browser doesn't stall the editor
} m_textures;
void render_texture_browser();
// texture of the image of the item of the browser, 0 while there's none
std::uint64_t texture_thumbnail(texture_browser::item const &Item);
// takes a material dragged from the texture browser onto the item drawn last. returns: true if one was dropped, its name in Name
static bool material_drop(std::string &Name);
// the same, for a text field
static bool material_drop(char *Buffer, std::size_t Size);
// replaces the chosen triangles of the chosen terrain files with chunks of heightmap terrain of their shape, painted with
// their materials; the files lose them on save. returns: summary for the user
std::string convert_ground();
void render_ground_conversion(); void render_ground_conversion();
// handles a click of the chunk, spacing and water tools (Shift: the other way round) // handles a click of the chunk, spacing and water tools (Shift: the other way round)
void handle_terrain_click(bool const Shift); void handle_terrain_click(bool const Shift);

View File

@@ -674,54 +674,84 @@ void editor_mode::profile_restore_ground()
void editor_mode::render_ground_conversion() void editor_mode::render_ground_conversion()
{ {
auto &conversion{m_ground_conversion}; auto &conversion{m_ground_conversion};
ImGui::PushID("ground_conversion");
if (false == conversion.open) if (false == conversion.open)
{ {
if (ImGui::Button(STR_C("Convert the scenery ground to heightmap terrain..."))) if (ImGui::Button(STR_C("Convert the terrain files to heightmap terrain...")))
{ {
conversion.open = true; conversion.open = true;
conversion.materials = ground_materials(); load_ground_files();
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()) if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the chosen materials with heightmap terrain of the same shape,\nwhich can be shaped, sculpted and painted; the scenery files lose these triangles on save")); ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the terrain files of the scenery (terrain directive, .txtf/.btf) with heightmap terrain\n"
"of the same shape, painted with their materials; the files lose these triangles on save"));
if (false == conversion.status.empty()) if (false == conversion.status.empty())
ImGui::TextWrapped("%s", conversion.status.c_str()); ImGui::TextWrapped("%s", conversion.status.c_str());
ImGui::PopID();
return; return;
} }
ImGui::TextWrapped("%s", STR_C("Materials of the scenery triangles: tick the ones which are the ground")); if (conversion.files.empty())
if (conversion.materials.empty()) ImGui::TextWrapped("%s", STR_C("The scenery has no terrain files: no terrain directive names a .txtf or .btf file"));
ImGui::TextDisabled("%s", STR_C("The scenery has no triangles")); else
for (auto const &entry : conversion.materials) ImGui::TextUnformatted(STR_C("Terrain files of the scenery:"));
for (std::size_t index = 0; index < conversion.files.size(); ++index)
{ {
auto const *material{GfxRenderer->Material(entry.first)}; auto &source{conversion.files[index]};
auto const name{material != nullptr ? material->GetName() : std::string{"?"}}; ImGui::PushID(static_cast<int>(index));
bool chosen{conversion.chosen.count(entry.first) != 0}; ImGui::BeginDisabled(false == source.good);
ImGui::PushID(static_cast<int>(entry.first)); ImGui::Checkbox(source.file.c_str(), &source.chosen);
if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), name.c_str(), entry.second).c_str(), &chosen)) ImGui::EndDisabled();
ImGui::SameLine();
if (source.good)
ImGui::TextDisabled(source.text ? STR_C("(text, %zu triangles)") : STR_C("(binary, %zu triangles)"), source.count);
else
ImGui::TextDisabled("(%s)", source.message.c_str());
ImGui::PopID();
}
// the materials of the chosen files
std::vector<std::size_t> counts(conversion.names.size(), 0);
for (auto const &source : conversion.files)
if (source.good && source.chosen)
for (auto index = source.first; index < source.first + source.count; ++index)
++counts[conversion.materials[index]];
if (false == conversion.names.empty())
ImGui::TextUnformatted(STR_C("Materials: tick the ones which are the ground"));
for (std::size_t index = 0; index < conversion.names.size(); ++index)
{
if (counts[index] == 0)
continue;
bool chosen{conversion.chosen.count(index) != 0};
ImGui::PushID(static_cast<int>(index) + 0x10000);
if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), conversion.names[index].c_str(), counts[index]).c_str(), &chosen))
{ {
if (chosen) if (chosen)
conversion.chosen.insert(entry.first); conversion.chosen.insert(index);
else else
conversion.chosen.erase(entry.first); conversion.chosen.erase(index);
} }
ImGui::PopID(); ImGui::PopID();
} }
ImGui::TextWrapped("%s", STR_C("The chunks are covered with the material which covers the most, added to the palette of the terrain. Until saved, loading the scenery again takes it all back.")); ImGui::TextWrapped("%s", STR_C("The chunks are painted with the materials of the triangles, up to 8 of them per chunk, added to the palette of the terrain. "
if (conversion.chosen.empty()) "Until saved, loading the scenery again takes it all back."));
ImGui::TextDisabled("%s", STR_C("Tick at least one material")); auto const ready{std::any_of(conversion.files.begin(), conversion.files.end(), [](auto const &Source) { return Source.good && Source.chosen; }) && false == conversion.chosen.empty()};
else if (ImGui::Button(STR_C("Convert"))) ImGui::BeginDisabled(false == ready);
if (ImGui::Button(STR_C("Convert")))
{ {
conversion.status = convert_ground(conversion.chosen); conversion.status = convert_ground();
conversion.open = false; conversion.open = false;
profile_resample(); profile_resample();
} }
ImGui::EndDisabled();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button(STR_C("Cancel"))) if (ImGui::Button(STR_C("Cancel")))
conversion.open = false; conversion.open = false;
if (false == conversion.open)
{
// the triangles take memory, they're read again the next time
conversion.triangles = {};
conversion.materials = {};
}
ImGui::PopID();
} }
void editor_mode::render_profile_earthworks() void editor_mode::render_profile_earthworks()

View File

@@ -72,6 +72,10 @@ http://mozilla.org/MPL/2.0/.
// spread <track> <stop 0..2> <reach> logs the paths the spread from the track takes // spread <track> <stop 0..2> <reach> logs the paths the spread from the track takes
// newscenery the editor starts again with the wizard of a new scenery // newscenery the editor starts again with the wizard of a new scenery
// vehicles logs each vehicle: its head, facing, and what its couplers hold // vehicles logs each vehicle: its head, facing, and what its couplers hold
// terrainmode the terrain field of work, with its texture browser
// terrainsculpt <x> <z> <radius> <metres>, terrainpaint <x> <z> <radius> <palette entry> as a stroke of the brush
// terrainprobe <x> <z> logs the height of the heightmap terrain and the materials of the chunk
// terrainconvert converts every material of the terrain files of the scenery
// screenshot // screenshot
// save // save
// log <text> // log <text>
@@ -1134,6 +1138,53 @@ void editor_mode::selftest_step()
} }
} }
} }
else if (command == "terrainmode")
{
m_terrain_open = true;
WriteLog("SELFTEST terrainmode");
}
else if (command == "terrainsculpt" || command == "terrainpaint")
{
double x{0.0}, z{0.0}, radius{10.0}, value{1.0};
words >> x >> z >> radius >> value;
auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) : m_streamer.paint(x, z, radius, 1.0, static_cast<std::uint16_t>(value))};
forget_ground();
WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed"));
}
else if (command == "terrainprobe")
{
double x{0.0}, z{0.0}, height{0.0};
words >> x >> z;
auto const *chunk{m_streamer.terrain_at(x, z)};
std::string materials;
if (chunk != nullptr)
for (std::size_t slot = 0; slot < std::max<std::size_t>(1, chunk->data().layer_count()); ++slot)
{
auto const layer{chunk->data().layers[slot]};
auto const &palette{m_streamer.manifest().layers};
materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " ";
}
auto const found{m_streamer.height_at(x, z, height)};
WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str()));
}
else if (command == "terrainconvert")
{
load_ground_files();
for (auto const &source : m_ground_conversion.files)
{
glm::dvec3 low{1e30}, high{-1e30};
for (auto index = source.first; index < source.first + source.count; ++index)
for (auto const &vertex : m_ground_conversion.triangles[index])
{
low = glm::min(low, vertex);
high = glm::max(high, vertex);
}
WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message +
format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z));
}
m_ground_conversion.status = convert_ground();
WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status);
}
else if (command == "quit") else if (command == "quit")
{ {
if (false == test.quitting) if (false == test.quitting)

View File

@@ -0,0 +1,279 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "application/editormode.h"
#include "rendering/renderer.h"
#include "utilities/Globals.h"
#include "utilities/utilities.h"
#include "utilities/translation.h"
#include "imgui/imgui.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <map>
namespace
{
// payload of a material dragged from the texture browser: its name, with the terminating zero
char const *const material_payload{"EU07_MATERIAL"};
// files the browser shows: textures, and the materials, which are kept with them
bool texture_file(std::string const &Extension)
{
return Extension == ".dds" || Extension == ".tga" || Extension == ".png" || Extension == ".bmp" || Extension == ".jpg" || Extension == ".ktx" || Extension == ".tex";
}
std::string lowercase(std::string Text)
{
std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast<char>(std::tolower(Character)); });
return Text;
}
// the text cut to fit the width, with an ellipsis
std::string fitted(std::string const &Text, float const Width)
{
if (ImGui::CalcTextSize(Text.c_str()).x <= Width)
return Text;
auto result{Text};
while (false == result.empty() && ImGui::CalcTextSize((result + "...").c_str()).x > Width)
result.pop_back();
return result + "...";
}
} // namespace
bool editor_mode::material_drop(std::string &Name)
{
if (false == ImGui::BeginDragDropTarget())
return false;
auto dropped{false};
if (auto const *payload{ImGui::AcceptDragDropPayload(material_payload)})
{
auto const *data{static_cast<char const *>(payload->Data)};
Name.assign(data, std::find(data, data + payload->DataSize, '\0'));
dropped = false == Name.empty();
}
ImGui::EndDragDropTarget();
return dropped;
}
bool editor_mode::material_drop(char *Buffer, std::size_t const Size)
{
std::string name;
if (Size == 0 || false == material_drop(name))
return false;
auto const length{std::min(name.size(), Size - 1)};
std::memcpy(Buffer, name.data(), length);
Buffer[length] = '\0';
return true;
}
std::uint64_t editor_mode::texture_thumbnail(texture_browser::item const &Item)
{
auto &browser{m_textures};
auto lookup{browser.thumbnails.find(Item.name)};
if (lookup == browser.thumbnails.end())
{
// reading a texture takes a moment, a few of them go in a frame
if (browser.loads >= 2)
return 0;
++browser.loads;
texture_handle texture{null_handle};
if (Item.material)
{
auto const material{GfxRenderer->Fetch_Material(Item.name)};
if (auto const *data{GfxRenderer->Material(material)}; material != null_handle && data != nullptr)
texture = data->GetTexture(0);
}
else
texture = GfxRenderer->Fetch_Texture(Item.name, true);
lookup = browser.thumbnails.emplace(Item.name, texture).first;
}
if (lookup->second == null_handle)
return 0;
auto &texture{GfxRenderer->Texture(lookup->second)};
if (false == texture.get_is_ready())
texture.create();
return texture.get_is_ready() ? static_cast<std::uint64_t>(texture.get_id()) : 0;
}
void editor_mode::render_texture_browser()
{
auto &browser{m_textures};
browser.loads = 0;
if (false == browser.listed)
{
browser.listed = true;
browser.folders.clear();
browser.items.clear();
std::map<std::string, texture_browser::item> items;
std::error_code error;
for (std::filesystem::directory_iterator entry{std::string{paths::textures} + browser.folder, error}, end; false == static_cast<bool>(error) && entry != end; entry.increment(error))
{
auto const path{entry->path()};
if (entry->is_directory(error))
{
browser.folders.push_back(path.filename().string());
continue;
}
auto const extension{lowercase(path.extension().string())};
auto const material{extension == ".mat"};
if (false == material && false == texture_file(extension))
continue;
auto const stem{path.stem().string()};
auto &item{items[stem]};
item.name = browser.folder + stem;
item.label = stem;
item.material |= material;
}
for (auto &item : items)
browser.items.push_back(std::move(item.second));
std::sort(browser.folders.begin(), browser.folders.end());
}
// where the browser is, and the folders under it
ImGui::BeginDisabled(browser.folder.empty());
if (ImGui::Button(STR_C("Up")))
{
auto folder{browser.folder};
if (false == folder.empty())
folder.pop_back();
auto const slash{folder.rfind('/')};
browser.folder = (slash == std::string::npos ? std::string{} : folder.substr(0, slash + 1));
browser.listed = false;
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button(STR_C("Refresh")))
{
browser.listed = false;
browser.thumbnails.clear();
}
ImGui::SameLine();
ImGui::TextUnformatted((std::string{paths::textures} + browser.folder).c_str());
ImGui::SetNextItemWidth(std::max(80.0f, ImGui::GetContentRegionAvail().x * 0.5f));
ImGui::InputTextWithHint("##texturefilter", STR_C("Filter"), browser.filter, IM_ARRAYSIZE(browser.filter));
ImGui::SameLine();
ImGui::SetNextItemWidth(std::max(60.0f, ImGui::GetContentRegionAvail().x));
ImGui::SliderFloat("##texturesize", &browser.size, 48.0f, 256.0f, "%.0f px");
if (false == browser.folders.empty())
{
// the folders flow along the width of the window
auto const right{ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x};
for (std::size_t index = 0; index < browser.folders.size(); ++index)
{
auto const label{"[" + browser.folders[index] + "]"};
auto const width{ImGui::CalcTextSize(label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2.0f};
if (index > 0)
{
ImGui::SameLine();
if (ImGui::GetCursorScreenPos().x + width > right)
ImGui::NewLine();
}
ImGui::PushID(static_cast<int>(index));
if (ImGui::SmallButton(label.c_str()))
{
browser.folder += browser.folders[index] + "/";
browser.listed = false;
}
ImGui::PopID();
}
if (false == browser.listed)
{
// the list is made anew in the next frame
return;
}
}
ImGui::Separator();
std::vector<texture_browser::item const *> shown;
auto const filter{lowercase(browser.filter)};
for (auto const &item : browser.items)
if (filter.empty() || lowercase(item.label).find(filter) != std::string::npos)
shown.push_back(&item);
if (shown.empty())
{
ImGui::TextDisabled("%s", STR_C("No textures here"));
return;
}
ImGui::TextDisabled("%s", STR_C("Drag a texture onto a field of a material; double click: add it to the palette of the terrain"));
ImGui::BeginChild("##texturecards");
auto const &style{ImGui::GetStyle()};
auto const side{std::max(16.0f, browser.size * std::max(1.0f, Global.ui_scale))};
auto const cellheight{side + ImGui::GetTextLineHeight() + style.ItemInnerSpacing.y};
auto const columns{std::max(1, static_cast<int>((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (side + style.ItemSpacing.x)))};
auto const rows{(static_cast<int>(shown.size()) + columns - 1) / columns};
auto *drawlist{ImGui::GetWindowDrawList()};
ImGuiListClipper clipper;
clipper.Begin(rows, cellheight + style.ItemSpacing.y);
while (clipper.Step())
{
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row)
{
for (int column = 0; column < columns; ++column)
{
auto const index{static_cast<std::size_t>(row) * columns + column};
if (index >= shown.size())
break;
auto const &item{*shown[index]};
if (column > 0)
ImGui::SameLine();
ImGui::PushID(item.name.c_str());
ImGui::BeginGroup();
auto const corner{ImGui::GetCursorScreenPos()};
ImGui::InvisibleButton("##card", ImVec2(side, cellheight));
auto const hovered{ImGui::IsItemHovered()};
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
browser.selected = item.name;
std::snprintf(m_terrain_material, IM_ARRAYSIZE(m_terrain_material), "%s", item.name.c_str());
}
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && m_streamer.active())
m_terrain_layer = m_streamer.layer(item.name, m_terrain_material_size);
auto const texture{texture_thumbnail(item)};
if (ImGui::BeginDragDropSource())
{
ImGui::SetDragDropPayload(material_payload, item.name.c_str(), item.name.size() + 1);
if (texture != 0)
ImGui::Image(static_cast<ImTextureID>(texture), ImVec2(64.0f, 64.0f), ImVec2(0, 1), ImVec2(1, 0));
ImGui::TextUnformatted(item.name.c_str());
ImGui::EndDragDropSource();
}
ImVec2 const low{corner.x, corner.y}, high{corner.x + side, corner.y + side};
if (texture != 0)
drawlist->AddImage(static_cast<ImTextureID>(texture), low, high, ImVec2(0, 1), ImVec2(1, 0));
else
{
drawlist->AddRectFilled(low, high, ImGui::GetColorU32(ImGuiCol_FrameBg));
drawlist->AddText(ImVec2(low.x + style.FramePadding.x, low.y + style.FramePadding.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), "...");
}
if (item.material)
drawlist->AddText(ImVec2(low.x + style.FramePadding.x, high.y - ImGui::GetTextLineHeight() - style.FramePadding.y), IM_COL32(255, 220, 120, 255), "mat");
if (browser.selected == item.name || hovered)
drawlist->AddRect(low, high, ImGui::GetColorU32(browser.selected == item.name ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), 0.0f, 0, 2.0f);
drawlist->AddText(ImVec2(low.x, high.y + style.ItemInnerSpacing.y), ImGui::GetColorU32(ImGuiCol_Text), fitted(item.label, side).c_str());
if (hovered)
{
ImGui::BeginTooltip();
ImGui::TextUnformatted(item.name.c_str());
ImGui::TextDisabled("%s", item.material ? STR_C("material") : STR_C("texture"));
ImGui::EndTooltip();
}
ImGui::EndGroup();
ImGui::PopID();
}
}
}
ImGui::EndChild();
}

View File

@@ -128,6 +128,13 @@ class editor_ui : public ui_layer
void set_workspace(workspace const Workspace) void set_workspace(workspace const Workspace)
{ {
m_workspace = Workspace; m_workspace = Workspace;
auto const &bank{m_workspacebank[static_cast<std::size_t>(Workspace)]};
m_nodebankpanel.stand_in(bank.first, bank.second);
}
// content of the window of the node bank while the field of work is chosen, in place of the node bank
void set_workspace_bank(workspace const Workspace, std::function<void()> Content, std::string Title)
{
m_workspacebank[static_cast<std::size_t>(Workspace)] = {std::move(Content), std::move(Title)};
} }
void set_workspace_options(workspace const Workspace, std::function<void()> Tools, std::function<void()> Selection = {}) void set_workspace_options(workspace const Workspace, std::function<void()> Tools, std::function<void()> Selection = {})
{ {
@@ -205,6 +212,7 @@ class editor_ui : public ui_layer
workspace m_workspace{workspace::none}; workspace m_workspace{workspace::none};
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspacetools; std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspacetools;
std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspaceselection; std::array<std::function<void()>, static_cast<std::size_t>(workspace::count_)> m_workspaceselection;
std::array<std::pair<std::function<void()>, std::string>, static_cast<std::size_t>(workspace::count_)> m_workspacebank;
// scene window: the models of the scenery by layer and group, rebuilt when their number changes // scene window: the models of the scenery by layer and group, rebuilt when their number changes
struct scene_entry struct scene_entry
{ {

View File

@@ -934,6 +934,14 @@ void nodebank_panel::render()
{ {
ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y)); ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y));
} }
if (m_standin)
{
// the same window, so it keeps its place in the dock
if (ImGui::Begin((m_standintitle + "###" + name()).c_str(), nullptr, flags))
m_standin();
ImGui::End();
return;
}
auto const panelname{(title.empty() ? name() : title) + "###" + name()}; auto const panelname{(title.empty() ? name() : title) + "###" + name()};
if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) if (true == ImGui::Begin(panelname.c_str(), nullptr, flags))

View File

@@ -156,6 +156,13 @@ class nodebank_panel : public ui_panel
std::string set_name(model_set_ref const &Ref, char const *Manuallabel) const; std::string set_name(model_set_ref const &Ref, char const *Manuallabel) const;
// shows the set manager window, optionally with specified user set selected // shows the set manager window, optionally with specified user set selected
void open_sets_window(int const Setid = 0); void open_sets_window(int const Setid = 0);
// content drawn in the window of the node bank in its place, with its own title (a field of work of the editor uses the window
// for its own browser); none: the node bank
void stand_in(std::function<void()> Content, std::string Title)
{
m_standin = std::move(Content);
m_standintitle = std::move(Title);
}
private: private:
@@ -183,6 +190,8 @@ class nodebank_panel : public ui_panel
// true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take // true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take
bool node_selected() const; bool node_selected() const;
// members: // members:
std::function<void()> m_standin;
std::string m_standintitle;
std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank; std::vector<std::pair<std::string, std::shared_ptr<std::string>>> m_nodebank;
char m_nodesearch[128]; char m_nodesearch[128];
std::shared_ptr<std::string> m_selectedtemplate; std::shared_ptr<std::string> m_selectedtemplate;
@@ -295,4 +304,4 @@ class functions_panel : public ui_panel
scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel
std::string m_groupprefix; std::string m_groupprefix;
std::vector<text_line> m_grouplines; std::vector<text_line> m_grouplines;
}; };

View File

@@ -120,13 +120,15 @@ class ground_mesh
} }
private: private:
// NOTE: clamped before the conversion: a rectangle asked about can reach far beyond the mesh (to infinity even),
// and a float out of the range of int doesn't turn into anything sensible
int column(float const X) const int column(float const X) const
{ {
return std::clamp(static_cast<int>(std::floor((X - m_low.x) / m_cellsize)), 0, m_columns - 1); return static_cast<int>(std::clamp(std::floor((X - m_low.x) / m_cellsize), 0.f, static_cast<float>(m_columns - 1)));
} }
int row(float const Z) const int row(float const Z) const
{ {
return std::clamp(static_cast<int>(std::floor((Z - m_low.y) / m_cellsize)), 0, m_rows - 1); return static_cast<int>(std::clamp(std::floor((Z - m_low.y) / m_cellsize), 0.f, static_cast<float>(m_rows - 1)));
} }
// cells the bounds of a triangle reach: first column, first row, last column, last row // cells the bounds of a triangle reach: first column, first row, last column, last row
std::array<int, 4> cells(triangle const &Triangle) const std::array<int, 4> cells(triangle const &Triangle) const

View File

@@ -136,6 +136,8 @@ void terrain_streamer::close()
m_loading.clear(); m_loading.clear();
m_removed.clear(); m_removed.clear();
m_palette.clear(); m_palette.clear();
m_recording = 0;
m_change = change();
m_active = false; m_active = false;
m_name.clear(); m_name.clear();
m_directory.clear(); m_directory.clear();
@@ -159,6 +161,8 @@ void terrain_streamer::reset()
m_removed.clear(); m_removed.clear();
m_palette.clear(); m_palette.clear();
clear_water(false); clear_water(false);
m_recording = 0;
m_change = change();
m_active = false; m_active = false;
m_name.clear(); m_name.clear();
m_directory.clear(); m_directory.clear();
@@ -898,6 +902,24 @@ bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::function<fl
for (int iz = 0; iz <= data->cells; ++iz) for (int iz = 0; iz <= data->cells; ++iz)
for (int ix = 0; ix <= data->cells; ++ix) for (int ix = 0; ix <= data->cells; ++ix)
data->heights[static_cast<std::size_t>(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step); data->heights[static_cast<std::size_t>(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step);
return add_chunk(std::move(data));
}
bool terrain_streamer::add_chunk(std::shared_ptr<heightmap::chunk_data> Data)
{
if (Data == nullptr || false == Data->valid())
return false;
auto const key{Data->key};
if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0)
return false;
if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z}))
return false;
for (auto &layer : Data->layers)
if (layer != heightmap::no_layer && layer >= m_manifest.layers.size())
layer = 0;
auto data{std::move(Data)};
auto const low{data->corner()};
remember(key, nullptr);
auto chunk{std::make_unique<editor_terrain>(std::move(data))}; auto chunk{std::make_unique<editor_terrain>(std::move(data))};
chunk->m_modified = true; chunk->m_modified = true;
auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
@@ -915,6 +937,12 @@ bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::function<fl
void terrain_streamer::remove_chunk(int Cx, int Cz) void terrain_streamer::remove_chunk(int Cx, int Cz)
{ {
chunk_key const key{Cx, Cz}; chunk_key const key{Cx, Cz};
if (recording())
{
// the data is needed to bring the chunk back
if (auto const *chunk{load_now(key)})
remember(*chunk);
}
auto const lookup{m_chunks.find(key)}; auto const lookup{m_chunks.find(key)};
if (lookup != m_chunks.end()) if (lookup != m_chunks.end())
{ {
@@ -931,6 +959,7 @@ void terrain_streamer::spacing(int Cx, int Cz, float Spacing)
auto *chunk{load_now({Cx, Cz})}; auto *chunk{load_now({Cx, Cz})};
if (chunk == nullptr || heightmap::cells_for(Spacing) == chunk->cells()) if (chunk == nullptr || heightmap::cells_for(Spacing) == chunk->cells())
return; return;
remember(*chunk);
chunk->replace(chunk->data().resampled(Spacing), true, false); chunk->replace(chunk->data().resampled(Spacing), true, false);
auto const low{chunk->data().corner()}; auto const low{chunk->data().corner()};
apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size}); apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size});
@@ -957,6 +986,7 @@ bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std
shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]); shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast<std::size_t>(iz) * current.side() + ix]);
if (false == shaped) if (false == shaped)
continue; continue;
remember(*chunk);
auto &data{chunk->edit(true, false)}; auto &data{chunk->edit(true, false)};
(data.base.empty() ? data.heights : data.base) = std::move(heights); (data.base.empty() ? data.heights : data.base) = std::move(heights);
if (false == data.base.empty()) if (false == data.base.empty())
@@ -1081,6 +1111,7 @@ bool terrain_streamer::paint(double X, double Z, double Radius, double Strength,
if (false == painted) if (false == painted)
continue; continue;
data->compact_layers(); data->compact_layers();
remember(*chunk);
chunk->replace(std::move(data), false, true); chunk->replace(std::move(data), false, true);
changed = true; changed = true;
} }
@@ -1092,6 +1123,7 @@ std::uint16_t terrain_streamer::layer(std::string const &Material, float const S
for (std::size_t i = 0; i < m_manifest.layers.size(); ++i) for (std::size_t i = 0; i < m_manifest.layers.size(); ++i)
if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size)) if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size))
return static_cast<std::uint16_t>(i); return static_cast<std::uint16_t>(i);
remember_manifest();
m_manifest.layers.push_back({Material, std::max(0.f, Size)}); m_manifest.layers.push_back({Material, std::max(0.f, Size)});
m_manifestchanged = true; m_manifestchanged = true;
return static_cast<std::uint16_t>(m_manifest.layers.size() - 1); return static_cast<std::uint16_t>(m_manifest.layers.size() - 1);
@@ -1101,6 +1133,7 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size)
{ {
if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size) if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size)
return; return;
remember_manifest();
m_manifest.layers[Layer].size = std::max(0.f, Size); m_manifest.layers[Layer].size = std::max(0.f, Size);
m_manifestchanged = true; m_manifestchanged = true;
// the materials of the chunks using it are made again // the materials of the chunks using it are made again
@@ -1109,6 +1142,19 @@ void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size)
entry.second->m_paintedversion = 0; entry.second->m_paintedversion = 0;
} }
void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material)
{
if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material)
return;
remember_manifest();
m_manifest.layers[Layer].material = Material;
m_manifestchanged = true;
m_palette.erase(Layer);
for (auto &entry : m_chunks)
if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0))
entry.second->m_paintedversion = 0;
}
void terrain_streamer::compress(bool const State) void terrain_streamer::compress(bool const State)
{ {
if (m_manifest.compress == State) if (m_manifest.compress == State)
@@ -1128,6 +1174,7 @@ void terrain_streamer::default_spacing(float const Spacing)
void terrain_streamer::water(std::vector<heightmap::water_body> Water) void terrain_streamer::water(std::vector<heightmap::water_body> Water)
{ {
remember_manifest();
m_manifest.water = std::move(Water); m_manifest.water = std::move(Water);
m_manifestchanged = true; m_manifestchanged = true;
m_waterchanged = true; m_waterchanged = true;
@@ -1155,12 +1202,14 @@ void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &
// no modifier reaches the chunk (any more): its own heights are what's left // no modifier reaches the chunk (any more): its own heights are what's left
if (false == current.base.empty()) if (false == current.base.empty())
{ {
remember(Chunk);
auto &data{Chunk.edit(true, false)}; auto &data{Chunk.edit(true, false)};
data.heights = std::move(data.base); data.heights = std::move(data.base);
data.base.clear(); data.base.clear();
} }
return; return;
} }
remember(Chunk);
auto &data{Chunk.edit(true, false)}; auto &data{Chunk.edit(true, false)};
if (data.base.empty()) if (data.base.empty())
data.base = data.heights; data.base = data.heights;
@@ -1187,6 +1236,7 @@ void terrain_streamer::apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &
void terrain_streamer::modifier(heightmap::modifier Modifier) void terrain_streamer::modifier(heightmap::modifier Modifier)
{ {
remember_manifest();
auto bounds{Modifier.bounds()}; auto bounds{Modifier.bounds()};
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })}; auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })};
if (existing != m_manifest.modifiers.end()) if (existing != m_manifest.modifiers.end())
@@ -1208,6 +1258,7 @@ bool terrain_streamer::remove_modifier(std::string const &Name)
auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })}; auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })};
if (existing == m_manifest.modifiers.end()) if (existing == m_manifest.modifiers.end())
return false; return false;
remember_manifest();
auto const bounds{existing->bounds()}; auto const bounds{existing->bounds()};
m_manifest.modifiers.erase(existing); m_manifest.modifiers.erase(existing);
m_manifestchanged = true; m_manifestchanged = true;
@@ -1222,6 +1273,103 @@ bool terrain_streamer::modified() const
return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); }); return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); });
} }
// undo
void terrain_streamer::begin_change()
{
if (m_recording++ == 0)
m_change = change();
}
terrain_streamer::change terrain_streamer::end_change()
{
if (m_recording == 0)
return change();
if (--m_recording > 0)
return change(); // the outer change goes on
change result{std::move(m_change)};
m_change = change();
return result;
}
void terrain_streamer::remember(chunk_key const &Key, heightmap::chunk_ptr const &Data)
{
if (m_recording > 0)
m_change.chunks.emplace(Key, Data); // only the first state counts
}
void terrain_streamer::remember_manifest()
{
if (m_recording == 0 || m_change.manifest)
return;
m_change.manifest = true;
m_change.layers = m_manifest.layers;
m_change.water = m_manifest.water;
m_change.modifiers = m_manifest.modifiers;
}
terrain_streamer::change terrain_streamer::restore(change const &Change)
{
change opposite;
if (false == m_active)
return opposite;
for (auto const &[key, data] : Change.chunks)
{
auto *current{load_now(key)};
opposite.chunks.emplace(key, current != nullptr ? current->shared() : nullptr);
if (data == nullptr)
{
if (current != nullptr)
{
undraw(*current);
m_chunks.erase(key);
m_removed.insert(key);
touch_neighbours(key);
}
continue;
}
if (current != nullptr)
{
current->replace(data, true, true);
}
else
{
auto chunk{std::make_unique<editor_terrain>(data)};
chunk->m_modified = true;
auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())};
auto const corner{data->corner()};
chunk->m_low = {corner.x, *low, corner.y};
chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size};
m_chunks.emplace(key, std::move(chunk));
m_removed.erase(key);
m_loading.erase(key);
}
touch_neighbours(key);
}
if (Change.manifest)
{
opposite.manifest = true;
opposite.layers = m_manifest.layers;
opposite.water = m_manifest.water;
opposite.modifiers = m_manifest.modifiers;
auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(),
[](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size; })};
m_manifest.layers = Change.layers;
m_manifest.modifiers = Change.modifiers;
m_manifest.water = Change.water;
m_manifestchanged = true;
m_waterchanged = true;
if (false == samelayers)
{
// the materials of the chunks are made anew from the palette as it was
m_palette.clear();
for (auto &entry : m_chunks)
entry.second->m_paintedversion = 0;
}
}
return opposite;
}
bool terrain_streamer::save(std::string *Error) bool terrain_streamer::save(std::string *Error)
{ {
if (false == m_active) if (false == m_active)

View File

@@ -84,6 +84,8 @@ class terrain_streamer
bool add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t Layer = 0); bool add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t Layer = 0);
// a chunk at (Cx, Cz), heights given for each grid point by Height (from its world position) // a chunk at (Cx, Cz), heights given for each grid point by Height (from its world position)
bool add_chunk(int Cx, int Cz, float Spacing, std::function<float(double X, double Z)> const &Height, std::uint16_t Layer = 0); bool add_chunk(int Cx, int Cz, float Spacing, std::function<float(double X, double Z)> const &Height, std::uint16_t Layer = 0);
// a chunk made elsewhere, at the place its key says; its layers are indices of the palette
bool add_chunk(std::shared_ptr<heightmap::chunk_data> Data);
void remove_chunk(int Cx, int Cz); void remove_chunk(int Cx, int Cz);
// changes the point spacing of the chunk at (Cx, Cz), heights resampled // changes the point spacing of the chunk at (Cx, Cz), heights resampled
void spacing(int Cx, int Cz, float Spacing); void spacing(int Cx, int Cz, float Spacing);
@@ -99,6 +101,8 @@ class terrain_streamer
// palette entry of the material, added to the palette if needed // palette entry of the material, added to the palette if needed
std::uint16_t layer(std::string const &Material, float Size = 0.f); std::uint16_t layer(std::string const &Material, float Size = 0.f);
void layer_size(std::uint16_t Layer, float Size); void layer_size(std::uint16_t Layer, float Size);
// gives the palette entry Layer another material; the chunks painted with it change with it
void layer_material(std::uint16_t Layer, std::string const &Material);
void compress(bool State); void compress(bool State);
void default_spacing(float Spacing); void default_spacing(float Spacing);
// bodies of water // bodies of water
@@ -110,6 +114,26 @@ class terrain_streamer
std::vector<heightmap::modifier> const &modifiers() const { return m_manifest.modifiers; } std::vector<heightmap::modifier> const &modifiers() const { return m_manifest.modifiers; }
// changed since the last save // changed since the last save
bool modified() const; bool modified() const;
// undo: the state of what a change touched, from before it
struct change
{
std::map<chunk_key, heightmap::chunk_ptr> chunks; // the data the chunks had; null: there was no chunk
bool manifest{false}; // the palette, the water and the modifiers below were changed
std::vector<heightmap::layer_def> layers;
std::vector<heightmap::water_body> water;
std::vector<heightmap::modifier> modifiers;
bool empty() const { return chunks.empty() && false == manifest; }
};
// from now on the state of what's changed is kept, until end_change(); nested calls join the change in progress
void begin_change();
// returns: the state from before the changes made since begin_change() (empty if nothing changed)
change end_change();
bool recording() const { return m_recording > 0; }
// puts the terrain back in the state recorded. returns: the state it replaced, to go forward again
change restore(change const &Change);
// writes the description and the changed pack files. returns: true on success, Error describing the failure // writes the description and the changed pack files. returns: true on success, Error describing the failure
bool save(std::string *Error = nullptr); bool save(std::string *Error = nullptr);
@@ -200,6 +224,10 @@ class terrain_streamer
void build_water(); void build_water();
void clear_water(bool const Touch); void clear_water(bool const Touch);
std::string pack_path(heightmap::pack_key const &Key) const; std::string pack_path(heightmap::pack_key const &Key) const;
// the state of a chunk (null: no chunk) or of the description is kept for the change in progress, if it's the first time
void remember(chunk_key const &Key, heightmap::chunk_ptr const &Data);
void remember(editor_terrain const &Chunk) { remember(Chunk.key(), Chunk.shared()); }
void remember_manifest();
bool m_active{false}; bool m_active{false};
std::string m_name; std::string m_name;
@@ -221,6 +249,9 @@ class terrain_streamer
gfx::geometrybank_handle m_waterbank{0, 0}; gfx::geometrybank_handle m_waterbank{0, 0};
bool m_waterchanged{false}; bool m_waterchanged{false};
int m_recording{0};
change m_change;
glm::dvec3 m_camera{0.0}; glm::dvec3 m_camera{0.0};
glm::dvec3 m_velocity{0.0}; glm::dvec3 m_velocity{0.0};
double m_cameratime{0.0}; double m_cameratime{0.0};

View File

@@ -329,11 +329,24 @@ bool opengl33_terrain_materials::copy_layer(layer &Layer)
if (slots[i] < 0 || material.textures[slots[i]] == null_handle) if (slots[i] < 0 || material.textures[slots[i]] == null_handle)
continue; continue;
auto &texture{m_textures.mark_as_used(material.textures[slots[i]])}; auto &texture{m_textures.mark_as_used(material.textures[slots[i]])};
if (false == texture.create()) if (false == texture.is_ready)
{ {
if (++Layer.attempts < copy_attempts) // a texture which is on the card already may have dropped its data after the upload (create() would refuse it then),
return false; // one released by the sweep or never read has to be read first
continue; // waited long enough, the layer goes without it if (texture.data_state == resource_state::none)
{
texture.reload_on_use = false;
texture.load();
}
if (texture.data_state == resource_state::failed)
continue;
if (false == texture.create())
{
if (++Layer.attempts < copy_attempts)
return false;
WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it");
continue; // waited long enough, the layer goes without it
}
} }
sources[i] = &texture; sources[i] = &texture;
} }

View File

@@ -499,12 +499,12 @@ void node_layers::shape(material_handle const Material, source_span const &Span)
m_shapes.push_back({current, Span, Material, false}); 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> node_layers::erase_shapes(std::set<material_handle> const &Materials, std::set<layer_handle> const *Layers)
{ {
std::pair<std::size_t, std::size_t> result{0, 0}; std::pair<std::size_t, std::size_t> result{0, 0};
for (auto &shape : m_shapes) for (auto &shape : m_shapes)
{ {
if (shape.erased || Materials.count(shape.material) == 0) if (shape.erased || Materials.count(shape.material) == 0 || (Layers != nullptr && Layers->count(shape.layer) == 0))
{ {
continue; continue;
} }

View File

@@ -267,9 +267,9 @@ class node_layers
{ {
return m_shapes; return m_shapes;
} }
// drops the definitions of the shapes of specified materials from the writable layer files on save. // drops the definitions of the shapes of specified materials from the writable layer files on save; only from specified layers, if given.
// returns: amount of the shapes to drop, and of the ones which stay as their files can't be rewritten // 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); std::pair<std::size_t, std::size_t> erase_shapes(std::set<material_handle> const &Materials, std::set<layer_handle> const *Layers = nullptr);
// indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive. // indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive.
// Infile: the directive comes straight from the layer file being loaded // Infile: the directive comes straight from the layer file being loaded
void initialization(source_span const &Span, bool Infile); void initialization(source_span const &Span, bool Infile);

View File

@@ -429,7 +429,9 @@ bool
shape_node::merge( shape_node &Shape ) { shape_node::merge( shape_node &Shape ) {
if( m_data.material != Shape.m_data.material if( m_data.material != Shape.m_data.material
|| m_data.lighting != Shape.m_data.lighting ) { || m_data.lighting != Shape.m_data.lighting
|| m_data.terrainfile != Shape.m_data.terrainfile ) {
// (shapes of different terrain files are kept apart, so the editor can tell which file the geometry comes from)
// can't merge nodes with different appearance // can't merge nodes with different appearance
return false; return false;
} }

View File

@@ -99,6 +99,8 @@ public:
gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer
std::vector<world_vertex> vertices; // world space source data of the geometry std::vector<world_vertex> vertices; // world space source data of the geometry
gfx::userdata_array userdata; gfx::userdata_array userdata;
// terrain file the shape comes from, see scene::terrain_file::reference_of(); 0: none, or not known (only the editor asks)
std::uint16_t terrainfile { 0 };
// methods: // methods:
// sends content of the struct to provided stream // sends content of the struct to provided stream
void void
@@ -152,6 +154,9 @@ public:
// sets whether the shape is drawn with the translucent geometry (used by generated geometry) // sets whether the shape is drawn with the translucent geometry (used by generated geometry)
void void
translucent( bool const Translucent ); translucent( bool const Translucent );
// notes the terrain file the shape comes from
void
source_file( std::uint16_t const Reference ) { m_data.terrainfile = Reference; }
// data access // data access
shapenode_data const & shapenode_data const &
data() const; data() const;

View File

@@ -23,6 +23,8 @@ http://mozilla.org/MPL/2.0/.
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <sstream>
#include <stack>
#include <unordered_map> #include <unordered_map>
/* /*
@@ -75,6 +77,7 @@ struct terrain_source
}; };
std::string file; std::string file;
std::vector<material> materials; std::vector<material> materials;
std::uint16_t reference{0}; // see terrain_file::reference_of()
}; };
namespace namespace
@@ -739,6 +742,233 @@ std::string terrain_file::extra(std::string const &Binaryfile)
return text; return text;
} }
std::vector<terrain_reference> &terrain_file::references()
{
static std::vector<terrain_reference> references;
return references;
}
std::uint16_t terrain_file::reference_of(std::string const &File)
{
auto file{File};
replace_slashes(file);
auto const &list{references()};
for (std::size_t index{0}; index < list.size() && index < std::numeric_limits<std::uint16_t>::max(); ++index)
{
auto const base{Global.asCurrentSceneryPath + list[index].file};
if (file.ends_with(base + ".txtf") || file.ends_with(base + ".btf"))
{
return static_cast<std::uint16_t>(index + 1);
}
}
return 0;
}
namespace
{
// world position of a vertex of a shape placed with specified rotation and offset, the way basic_region::insert() does it
glm::dvec3 place_vertex(glm::dvec3 Position, glm::vec3 const &Rotation, glm::dvec3 const &Offset)
{
if (Rotation != glm::vec3(0.f))
{
auto const rotation{glm::radians(glm::dvec3{Rotation})};
Position = glm::rotateZ<double>(Position, rotation.z);
Position = glm::rotateX<double>(Position, rotation.x);
Position = glm::rotateY<double>(Position, rotation.y);
}
return Position + Offset;
}
bool read_text_triangles(std::string const &Text, terrain_reference const &Reference, terrain_file::triangle_visitor const &Visit, std::string &Message)
{
conversion_scope scope;
std::stack<glm::dvec3> offsets;
offsets.push(Reference.offset);
auto rotation{Reference.rotation};
std::size_t count{0};
cParser input{Text, cParser::buffer_TEXT};
input.expandIncludes = false;
auto token{input.getToken<std::string>()};
while (false == token.empty())
{
if (token == "node")
{
node_data nodedata;
input.getTokens(4);
input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type;
if (nodedata.type == "triangles" || nodedata.type == "triangle_strip" || nodedata.type == "triangle_fan")
{
auto const shape{shape_node().import(input, nodedata)};
auto const &data{shape.data()};
auto const *material{GfxRenderer->Material(data.material)};
auto const name{material != nullptr ? material->GetName() : std::string{}};
for (std::size_t index{0}; index + 2 < data.vertices.size(); index += 3)
{
Visit(name, {place_vertex(data.vertices[index].position, rotation, offsets.top()), place_vertex(data.vertices[index + 1].position, rotation, offsets.top()),
place_vertex(data.vertices[index + 2].position, rotation, offsets.top())});
++count;
}
}
else
{
// anything else the file holds (the editor keeps sweeps there) is of no interest here
auto const end{"end" + nodedata.type};
auto rest{input.getToken<std::string>()};
while (false == rest.empty() && rest != end)
rest = input.getToken<std::string>();
}
}
else if (token == "origin")
{
glm::dvec3 offset;
input.getTokens(3);
input >> offset.x >> offset.y >> offset.z;
offsets.push(offset + offsets.top());
}
else if (token == "endorigin")
{
if (offsets.size() > 1)
{
offsets.pop();
}
}
else if (token == "rotate")
{
input.getTokens(3);
input >> rotation.x >> rotation.y >> rotation.z;
}
token = input.getToken<std::string>();
}
Message = std::to_string(count) + " triangle(s)";
return true;
}
bool read_binary_triangles(std::string const &Binaryfile, terrain_file::triangle_visitor const &Visit, std::string &Message)
{
std::string content;
if (false == read_file(Binaryfile, content))
{
Message = "can't read the file";
return false;
}
std::istringstream stream{content};
file_header header;
if (false == read_header(stream, header))
{
Message = "the file is of either unrecognized type or version";
return false;
}
std::vector<std::string> materials;
for (std::uint32_t index{0}; index < header.materialcount; ++index)
{
std::string name;
if (false == static_cast<bool>(std::getline(stream, name, '\0')))
{
Message = "the file is damaged";
return false;
}
materials.emplace_back(std::move(name));
}
auto const directorystart{static_cast<std::size_t>(stream.tellg())};
std::size_t const entrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)};
if (directorystart > content.size() || content.size() - directorystart < header.sectioncount * entrysize)
{
Message = "the file is damaged";
return false;
}
data_reader directory{content.data() + directorystart, header.sectioncount * entrysize};
std::size_t count{0};
for (std::uint32_t entry{0}; entry < header.sectioncount; ++entry)
{
auto const sectionindex{directory.get<std::uint32_t>()};
auto const offset{directory.get<std::uint64_t>()};
auto const size{directory.get<std::uint32_t>()};
directory.get<float>();
if (false == directory.good() || offset > content.size() || content.size() - offset < size)
{
Message = "the file is damaged";
return false;
}
// the vertices are relative to the centre of their section, see attach()
auto const column{static_cast<int>(sectionindex % EU07_REGIONSIDESECTIONCOUNT)};
auto const row{static_cast<int>(sectionindex / EU07_REGIONSIDESECTIONCOUNT)};
auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2};
glm::dvec3 const centre{static_cast<double>(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast<double>(centeroffset + row * EU07_SECTIONSIZE)};
data_reader reader{content.data() + offset, size};
if (reader.get<std::uint32_t>() != EU07_TERRAINBLOCK)
{
Message = "the file is damaged";
return false;
}
reader.get<std::uint32_t>(); // section index
auto shapecount{reader.get<std::uint32_t>()};
while (shapecount-- > 0 && reader.good())
{
auto const materialindex{reader.get<std::uint32_t>()};
// cell, ranges, colours, centre and radius
reader.skip(sizeof(std::uint32_t) + 2 * sizeof(double) + 12 * sizeof(float) + 3 * sizeof(double) + sizeof(float));
auto const vertexcount{reader.get<std::uint32_t>()};
if (false == reader.good() || false == reader.has(static_cast<std::size_t>(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= materials.size())
{
Message = "the file is damaged";
return false;
}
std::array<glm::dvec3, 3> triangle;
for (std::uint32_t vertex{0}; vertex < vertexcount; ++vertex)
{
float values[8];
reader.get(values, 8);
triangle[vertex % 3] = centre + glm::dvec3{values[0], values[1], values[2]};
if (vertex % 3 == 2)
{
Visit(materials[materialindex], triangle);
++count;
}
}
}
if (false == reader.good())
{
Message = "the file is damaged";
return false;
}
}
Message = std::to_string(count) + " triangle(s)";
return true;
}
} // namespace
bool terrain_file::read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message, bool *Text)
{
auto const textfile{Global.asCurrentSceneryPath + Reference.file + ".txtf"};
auto const binaryfile{Global.asCurrentSceneryPath + Reference.file + ".btf"};
std::string message;
auto result{false};
auto const fromtext{FileExists(textfile)};
if (fromtext)
{
std::string text;
result = read_file(textfile, text) && read_text_triangles(text, Reference, Visit, message);
if (message.empty())
message = "can't read the file";
}
else if (FileExists(binaryfile))
{
// the binary file holds the geometry where the text alone puts it; it stands in for files placed without offset or rotation only
result = read_binary_triangles(binaryfile, Visit, message);
}
else
{
message = "the file is missing";
}
if (Message != nullptr)
*Message = message;
if (Text != nullptr)
*Text = fromtext;
return result;
}
// makes content of specified binary terrain file a part of provided region // makes content of specified binary terrain file a part of provided region
bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred) bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred)
{ {
@@ -751,6 +981,7 @@ bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, b
} }
auto source{std::make_shared<terrain_source>()}; auto source{std::make_shared<terrain_source>()};
source->file = Binaryfile; source->file = Binaryfile;
source->reference = reference_of(Binaryfile);
source->materials.reserve(header.materialcount); source->materials.reserve(header.materialcount);
for (std::uint32_t index{0}; index < header.materialcount; ++index) for (std::uint32_t index{0}; index < header.materialcount; ++index)
{ {
@@ -878,6 +1109,7 @@ void terrain_file::load(basic_section &Section, terrain_block const &Block)
} }
shapedata.material = material.handle; shapedata.material = material.handle;
shapedata.translucent = material.translucent; shapedata.translucent = material.translucent;
shapedata.terrainfile = source.reference;
shapedata.vertices.resize(vertexcount); shapedata.vertices.resize(vertexcount);
for (auto &vertex : shapedata.vertices) for (auto &vertex : shapedata.vertices)
{ {

View File

@@ -9,8 +9,12 @@ http://mozilla.org/MPL/2.0/.
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <functional>
#include <string> #include <string>
#include <vector>
#include <glm/glm.hpp>
namespace scene namespace scene
{ {
@@ -22,10 +26,20 @@ struct terrain_block;
// binary terrain file (.btf): static geometry of a text terrain file (.txtf), arranged by the sections // binary terrain file (.btf): static geometry of a text terrain file (.txtf), arranged by the sections
// of the scene so each can be loaded on its own, when it comes into use. // of the scene so each can be loaded on its own, when it comes into use.
// the binary file records length and checksum of the text it was made from, and is made anew when these don't match // the binary file records length and checksum of the text it was made from, and is made anew when these don't match
// terrain file named by a terrain directive of the scenery, with the placement it was loaded with
struct terrain_reference
{
std::string file; // name relative to the scenery folder, without extension
glm::dvec3 offset{0.0};
glm::vec3 rotation{0.f};
};
class terrain_file class terrain_file
{ {
public: public:
// types // types
// receives a triangle of a terrain file: name of its material, the vertices in the world
using triangle_visitor = std::function<void(std::string const &Material, std::array<glm::dvec3, 3> const &Triangle)>;
// what a scenery is left with for a terrain file it refers to // what a scenery is left with for a terrain file it refers to
enum class state enum class state
{ {
@@ -47,6 +61,13 @@ class terrain_file
// loads specified piece of a binary terrain file into provided section // loads specified piece of a binary terrain file into provided section
static void load(basic_section &Section, terrain_block const &Block); static void load(basic_section &Section, terrain_block const &Block);
static std::string extra(std::string const &Binaryfile); static std::string extra(std::string const &Binaryfile);
// terrain files used by the scenery being loaded or shown, in the order the directives named them
static std::vector<terrain_reference> &references();
// number (counted from 1) of the reference whose text or binary file is the specified file, 0 if there's none
static std::uint16_t reference_of(std::string const &File);
// goes through the triangles of the terrain file of specified reference, read from the text file if it's there,
// otherwise from the binary one. returns: true on success, Message describing what was read or the failure
static bool read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message = nullptr, bool *Text = nullptr);
private: private:
// converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise // converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise

View File

@@ -57,6 +57,7 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) {
// drop any streamed editor terrain from a previously loaded scenery before the old region (and // drop any streamed editor terrain from a previously loaded scenery before the old region (and
// its sections, which those chunks referenced) is destroyed below // its sections, which those chunks referenced) is destroyed below
EditorTerrain.reset(); EditorTerrain.reset();
scene::terrain_file::references().clear();
// TODO: move initialization to separate routine so we can reuse it // TODO: move initialization to separate routine so we can reuse it
SafeDelete( Region ); SafeDelete( Region );
@@ -722,6 +723,9 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch
++Scratchpad.binary.geometry_imported; ++Scratchpad.binary.geometry_imported;
auto shape { scene::shape_node().import( Input, nodedata ) }; auto shape { scene::shape_node().import( Input, nodedata ) };
material = shape.data().material; material = shape.data().material;
if( true == Global.editor_session ) {
shape.source_file( scene::terrain_file::reference_of( Input.Name() ) );
}
simulation::Region->insert( simulation::Region->insert(
std::move( shape ), std::move( shape ),
Scratchpad, Scratchpad,
@@ -1188,6 +1192,11 @@ include_terrain_file( std::string File, std::string &Includes, scene::scratch_da
return; return;
} }
binary.terrain_textfiles.emplace_back( File ); binary.terrain_textfiles.emplace_back( File );
// the editor converts the triangles of the terrain files to heightmap terrain, it has to know them and where they're placed
scene::terrain_file::references().push_back( {
File,
( Scratchpad.location.offset.empty() ? glm::dvec3( 0.0 ) : Scratchpad.location.offset.top() ),
Scratchpad.location.rotation } );
auto const textfile { Global.asCurrentSceneryPath + File + ".txtf" }; auto const textfile { Global.asCurrentSceneryPath + File + ".txtf" };
auto const binaryfile { Global.asCurrentSceneryPath + File + ".btf" }; auto const binaryfile { Global.asCurrentSceneryPath + File + ".btf" };

View File

@@ -485,6 +485,11 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
return; return;
} }
// layers are named by their files relative to the scenery folder; the terrain files (.txtf) come from includes made up by
// the loader, relative to the folder of the simulator
auto layername{mPath + includefile};
layername = (layername.starts_with(Global.asCurrentSceneryPath) ? layername.substr(Global.asCurrentSceneryPath.size()) : includefile);
const bool isTerrain = contains(includefile, "_ter.scm"); const bool isTerrain = contains(includefile, "_ter.scm");
if (isTerrain && true == Global.file_binary_terrain_state) { if (isTerrain && true == Global.file_binary_terrain_state) {
WriteLog("SBT found, ignoring: " + includefile); WriteLog("SBT found, ignoring: " + includefile);
@@ -493,7 +498,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
if (sceneryLayers) if (sceneryLayers)
{ {
// the file is still a part of the scenery, even though its content comes from the binary terrain file // the file is still a part of the scenery, even though its content comes from the binary terrain file
scene::Layers.layer(scene::Layers.open(includefile, include_site())).binary = true; scene::Layers.layer(scene::Layers.open(layername, include_site())).binary = true;
scene::Layers.close(); scene::Layers.close();
} }
return; return;
@@ -537,7 +542,7 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri
{ {
auto site{include_site()}; auto site{include_site()};
site.parameters = false == mIncludeParser->parameters.empty(); site.parameters = false == mIncludeParser->parameters.empty();
scene::Layers.open(includefile, site); scene::Layers.open(layername, site);
mIncludeParser->mLayerFile = true; mIncludeParser->mLayerFile = true;
} }
} }